UNPKG

@jupyterlab/filebrowser

Version:
3,222 lines 126 kB
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Dialog, DOMUtils, showDialog, showErrorMessage } from '@jupyterlab/apputils';
import { PageConfig, PathExt, Time } from '@jupyterlab/coreutils';
import { isValidFileName, renameFile } from '@jupyterlab/docmanager';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { nullTranslator } from '@jupyterlab/translation';
import { caretDownIcon, caretUpIcon, classes, LabIcon } from '@jupyterlab/ui-components';
import { ArrayExt, filter, StringExt } from '@lumino/algorithm';
import { MimeData, PromiseDelegate } from '@lumino/coreutils';
import { ElementExt } from '@lumino/domutils';
import { DisposableDelegate } from '@lumino/disposable';
import { Drag } from '@lumino/dragdrop';
import { MessageLoop } from '@lumino/messaging';
import { Signal } from '@lumino/signaling';
import { h, VirtualDOM } from '@lumino/virtualdom';
import { Widget } from '@lumino/widgets';
/**
 * The class name added to DirListing widget.
 */
const DIR_LISTING_CLASS = 'jp-DirListing';
/**
 * The class name added to a dir listing header node.
 */
const HEADER_CLASS = 'jp-DirListing-header';
/**
 * The class name added to a dir listing list header cell.
 */
const HEADER_ITEM_CLASS = 'jp-DirListing-headerItem';
/**
 * The class name added to a header cell text node.
 */
const HEADER_ITEM_TEXT_CLASS = 'jp-DirListing-headerItemText';
/**
 * The class name added to a header cell icon node.
 */
const HEADER_ITEM_ICON_CLASS = 'jp-DirListing-headerItemIcon';
/**
 * The class name added to the dir listing content node.
 */
const CONTENT_CLASS = 'jp-DirListing-content';
/**
 * The class name added to dir listing content item.
 */
const ITEM_CLASS = 'jp-DirListing-item';
/**
 * The class name added to the listing item text cell.
 */
const ITEM_TEXT_CLASS = 'jp-DirListing-itemText';
/**
 * The class name added to the listing item text cell.
 */
const ITEM_NAME_COLUMN_CLASS = 'jp-DirListing-itemName';
/**
 * The class name added to the listing item icon cell.
 */
const ITEM_ICON_CLASS = 'jp-DirListing-itemIcon';
/**
 * The class name added to the listing item modified cell.
 */
const ITEM_MODIFIED_CLASS = 'jp-DirListing-itemModified';
/**
 * The class name added to the listing item file size cell.
 */
const ITEM_FILE_SIZE_CLASS = 'jp-DirListing-itemFileSize';
/**
 * The class name added to the listing item created cell.
 */
const ITEM_CREATED_CLASS = 'jp-DirListing-itemCreated';
/**
 * The class name added to the label element that wraps each item's checkbox and
 * the header's check-all checkbox.
 */
const CHECKBOX_WRAPPER_CLASS = 'jp-DirListing-checkboxWrapper';
/**
 * The class name added to the dir listing editor node.
 */
const EDITOR_CLASS = 'jp-DirListing-editor';
/**
 * The class name added to the name column header cell.
 */
const NAME_ID_CLASS = 'jp-id-name';
/**
 * The class name added to the modified column header cell.
 */
const MODIFIED_ID_CLASS = 'jp-id-modified';
/**
 * The class name added to the file size column header cell.
 */
const FILE_SIZE_ID_CLASS = 'jp-id-filesize';
/**
 * The class name added to the created column header cell.
 */
const CREATED_ID_CLASS = 'jp-id-created';
/**
 * The mime type for a contents drag object.
 */
const CONTENTS_MIME = 'application/x-jupyter-icontents';
/**
 * The mime type for a rich contents drag object.
 */
const CONTENTS_MIME_RICH = 'application/x-jupyter-icontentsrich';
/**
 * The class name added to drop targets.
 */
const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
/**
 * The class name added to selected rows.
 */
const SELECTED_CLASS = 'jp-mod-selected';
/**
 * The class name added to drag state icons to add space between the icon and the file name
 */
const DRAG_ICON_CLASS = 'jp-DragIcon';
/**
 * The class name added to column resize handle.
 */
const RESIZE_HANDLE_CLASS = 'jp-DirListing-resizeHandle';
/**
 * The class name added to the widget when there are items on the clipboard.
 */
const CLIPBOARD_CLASS = 'jp-mod-clipboard';
/**
 * The class name added to cut rows.
 */
const CUT_CLASS = 'jp-mod-cut';
/**
 * The class name added when there are more than one selected rows.
 */
const MULTI_SELECTED_CLASS = 'jp-mod-multiSelected';
/**
 * The class name added to indicate running notebook.
 */
const RUNNING_CLASS = 'jp-mod-running';
/**
 * The class name added to indicate the active element.
 */
const ACTIVE_CLASS = 'jp-mod-active';
/**
 * The class name added for a descending sort.
 */
const DESCENDING_CLASS = 'jp-mod-descending';
/**
 * The maximum duration between two key presses when selecting files by prefix.
 */
const PREFIX_APPEND_DURATION = 1000;
/**
 * The default width of the resize handle.
 */
const DEFAULT_HANDLE_WIDTH = 5;
/**
 * The threshold in pixels to start a drag event.
 */
const DRAG_THRESHOLD = 5;
/**
 * A boolean indicating whether the platform is Mac.
 */
const IS_MAC = !!navigator.platform.match(/Mac/i);
/**
 * The factory MIME type supported by lumino dock panels.
 */
const FACTORY_MIME = 'application/vnd.lumino.widget-factory';
/**
 * A widget which hosts a file list area.
 */
export class DirListing extends Widget {
    /**
     * Construct a new file browser directory listing widget.
     *
     * @param options The constructor options
     */
    constructor(options) {
        var _a;
        super({
            node: (options.renderer || DirListing.defaultRenderer).createNode()
        });
        this._items = [];
        this._sortedItems = [];
        this._sortState = {
            direction: 'ascending',
            key: 'name'
        };
        this._onItemOpened = new Signal(this);
        this._selectionChanged = new Signal(this);
        this._drag = null;
        this._dragData = null;
        this._resizeData = null;
        this._selectTimer = -1;
        this._isCut = false;
        this._prevPath = '';
        this._clipboard = [];
        this._softSelection = '';
        this.selection = Object.create(null);
        this._searchPrefix = '';
        this._searchPrefixTimer = -1;
        this._inRename = false;
        this._isDirty = false;
        this._hiddenColumns = new Set();
        this._columnSizes = {
            name: null,
            file_size: null,
            is_selected: null,
            last_modified: null,
            date_created: null
        };
        this._sortNotebooksFirst = false;
        this._sortFileNamesNaturally = true;
        this._allowSingleClick = false;
        this._allowDragDropUpload = true;
        // _focusIndex should never be set outside the range [0, this._items.length - 1]
        this._focusIndex = 0;
        this._focusPath = '';
        this._modifiedStyle = 'short';
        this._createdStyle = 'short';
        this._allUploaded = new Signal(this);
        this._width = null;
        this._state = null;
        this._contentScrollbarWidth = 0;
        this._contentSizeObserver = new ResizeObserver(this._onContentResize.bind(this));
        this._paddingWidth = 0;
        this._handleWidth = DEFAULT_HANDLE_WIDTH;
        this._lastRenderedState = new WeakMap();
        this.addClass(DIR_LISTING_CLASS);
        this.translator = options.translator || nullTranslator;
        this._trans = this.translator.load('jupyterlab');
        this._model = options.model;
        this._model.fileChanged.connect(this._onFileChanged, this);
        this._model.refreshed.connect(this._onModelRefreshed, this);
        this._model.pathChanged.connect(this._onPathChanged, this);
        this._editNode = document.createElement('input');
        this._editNode.className = EDITOR_CLASS;
        this._manager = this._model.manager;
        this._renderer = options.renderer || DirListing.defaultRenderer;
        this._state = options.state || null;
        this._allowDragDropUpload = (_a = options.allowDragDropUpload) !== null && _a !== void 0 ? _a : true;
        this._handleOpenFile =
            options.handleOpenFile ||
                ((path) => {
                    this._manager.openOrReveal(path);
                });
        // Get the width of the "modified" column
        this._updateModifiedSize(this.node);
        // Get the width of the "created" column
        this._updateCreatedSize(this.node);
        const headerNode = DOMUtils.findElement(this.node, HEADER_CLASS);
        // hide the file size column by default
        this._hiddenColumns.add('file_size');
        // hide the date created column by default
        this._hiddenColumns.add('date_created');
        this._renderer.populateHeaderNode(headerNode, this.translator, this._hiddenColumns, this._columnSizes);
        this._manager.activateRequested.connect(this._onActivateRequested, this);
    }
    /**
     * Dispose of the resources held by the directory listing.
     */
    dispose() {
        this._items.length = 0;
        this._sortedItems.length = 0;
        this._clipboard.length = 0;
        super.dispose();
    }
    /**
     * Get the model used by the listing.
     */
    get model() {
        return this._model;
    }
    /**
     * Get the dir listing header node.
     *
     * #### Notes
     * This is the node which holds the header cells.
     *
     * Modifying this node directly can lead to undefined behavior.
     */
    get headerNode() {
        return DOMUtils.findElement(this.node, HEADER_CLASS);
    }
    /**
     * Get the dir listing content node.
     *
     * #### Notes
     * This is the node which holds the item nodes.
     *
     * Modifying this node directly can lead to undefined behavior.
     */
    get contentNode() {
        return DOMUtils.findElement(this.node, CONTENT_CLASS);
    }
    /**
     * The renderer instance used by the directory listing.
     */
    get renderer() {
        return this._renderer;
    }
    /**
     * The current sort state.
     */
    get sortState() {
        return this._sortState;
    }
    /**
     * A signal fired when an item is opened.
     */
    get onItemOpened() {
        return this._onItemOpened;
    }
    /**
     * A signal fired when the selection changes.
     */
    get selectionChanged() {
        return this._selectionChanged;
    }
    /**
     * Create an iterator over the listing's selected items.
     *
     * @returns A new iterator over the listing's selected items.
     */
    selectedItems() {
        const items = this._sortedItems;
        return filter(items, item => this.selection[item.path]);
    }
    /**
     * Create an iterator over the listing's sorted items.
     *
     * @returns A new iterator over the listing's sorted items.
     */
    sortedItems() {
        return this._sortedItems[Symbol.iterator]();
    }
    /**
     * Sort the items using a sort condition.
     */
    sort(state) {
        this._sortedItems = Private.sort(this.model.items(), state, this._sortNotebooksFirst, this._sortFileNamesNaturally, this.translator);
        this._sortState = state;
        this.update();
        this._saveState();
    }
    /**
     * Rename the first currently selected item.
     *
     * @returns A promise that resolves with the new name of the item.
     */
    rename() {
        return this._doRename();
    }
    /**
     * Cut the selected items.
     */
    cut() {
        this._isCut = true;
        this._copy();
        this.update();
    }
    /**
     * Copy the selected items.
     */
    copy() {
        this._copy();
    }
    /**
     * Paste the items from the clipboard.
     *
     * @returns A promise that resolves when the operation is complete.
     */
    paste() {
        if (!this._clipboard.length) {
            this._isCut = false;
            return Promise.resolve(undefined);
        }
        const basePath = this._model.path;
        const promises = [];
        for (const path of this._clipboard) {
            if (this._isCut) {
                const localPath = this._manager.services.contents.localPath(path);
                const parts = localPath.split('/');
                const name = parts[parts.length - 1];
                const newPath = PathExt.join(basePath, name);
                promises.push(this._model.manager.rename(path, newPath));
            }
            else {
                promises.push(this._model.manager.copy(path, basePath));
            }
        }
        // Remove any cut modifiers.
        for (const item of this._items) {
            item.classList.remove(CUT_CLASS);
        }
        this._clipboard.length = 0;
        this._isCut = false;
        this.removeClass(CLIPBOARD_CLASS);
        return Promise.all(promises)
            .then(() => {
            return undefined;
        })
            .catch(error => {
            void showErrorMessage(this._trans._p('showErrorMessage', 'Paste Error'), error);
        });
    }
    /**
     * Delete the currently selected item(s).
     *
     * @returns A promise that resolves when the operation is complete.
     */
    async delete() {
        const deleteToTrash = PageConfig.getOption('delete_to_trash') === 'true';
        const items = this._sortedItems.filter(item => this.selection[item.path]);
        if (!items.length) {
            return;
        }
        const moveToTrashActionMessage = this._trans.__('Are you sure you want to move to trash: %1?', items[0].name);
        const deleteActionMessage = this._trans.__('Are you sure you want to permanently delete: %1?', items[0].name);
        const moveToTrashItemsActionMessage = this._trans._n('Are you sure you want to move to trash the %1 selected item?', 'Are you sure you want to move to trash the %1 selected items?', items.length);
        const deleteActionItemsMessage = 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);
        const actionMessage = deleteToTrash
            ? moveToTrashActionMessage
            : deleteActionMessage;
        const itemsActionMessage = deleteToTrash
            ? moveToTrashItemsActionMessage
            : deleteActionItemsMessage;
        const actionName = deleteToTrash
            ? this._trans.__('Move to Trash')
            : this._trans.__('Delete');
        const message = items.length === 1 ? actionMessage : itemsActionMessage;
        const result = await showDialog({
            title: actionName,
            body: message,
            buttons: [
                Dialog.cancelButton({ label: this._trans.__('Cancel') }),
                Dialog.warnButton({ label: actionName })
            ],
            // By default focus on "Cancel" to protect from accidental deletion
            // ("delete" and "Enter" are next to each other on many keyboards).
            defaultButton: 0
        });
        if (!this.isDisposed && result.button.accept) {
            await this._delete(items.map(item => item.path));
        }
        // Re-focus
        let focusIndex = this._focusIndex;
        const lastIndexAfterDelete = this._sortedItems.length - items.length - 1;
        if (focusIndex > lastIndexAfterDelete) {
            // If the focus index after deleting items is out of bounds, set it to the
            // last item.
            focusIndex = Math.max(0, lastIndexAfterDelete);
        }
        this._focusItem(focusIndex);
    }
    /**
     * Duplicate the currently selected item(s).
     *
     * @returns A promise that resolves when the operation is complete.
     */
    duplicate() {
        const basePath = this._model.path;
        const promises = [];
        for (const item of this.selectedItems()) {
            if (item.type !== 'directory') {
                promises.push(this._model.manager.copy(item.path, basePath));
            }
        }
        return Promise.all(promises)
            .then(() => {
            return undefined;
        })
            .catch(error => {
            void showErrorMessage(this._trans._p('showErrorMessage', 'Duplicate file'), error);
        });
    }
    /**
     * Select all listing items in the current directory.
     */
    async selectAll() {
        const items = this._model.items();
        const newSelection = {};
        for (const item of items) {
            newSelection[item.path] = true;
        }
        this.selection = newSelection;
        this._selectionChanged.emit();
        this.update();
    }
    /**
     * Download the currently selected item(s).
     */
    async download() {
        await Promise.all(Array.from(this.selectedItems())
            .filter(item => item.type !== 'directory')
            .map(item => this._model.download(item.path)));
    }
    /**
     * Restore the state of the file browser listing.
     *
     * @param id - The unique ID that is used to construct a state database key.
     *
     */
    async restore(id) {
        const key = `file-browser-${id}:columns`;
        const state = this._state;
        this._stateColumnsKey = key;
        if (!state) {
            return;
        }
        try {
            const columns = await state.fetch(key);
            if (!columns) {
                return;
            }
            const sizes = columns['sizes'];
            const sortState = columns['sortState'];
            // Set sort state before updating column sizes so that any
            // state save triggered by _updateColumnSizes includes the
            // correct sort state rather than the default.
            if (sortState) {
                this._sortState = sortState;
            }
            if (sizes) {
                for (const [key, size] of Object.entries(sizes)) {
                    this._columnSizes[key] = size;
                }
                this._updateColumnSizes();
            }
            // Sort items and rebuild the header to reflect the restored
            // sort state without calling sort() to avoid a redundant save.
            if (sortState) {
                this._sortedItems = Private.sort(this.model.items(), sortState, this._sortNotebooksFirst, this._sortFileNamesNaturally, this.translator);
                this.headerNode.innerHTML = '';
                this._renderer.populateHeaderNode(this.headerNode, this.translator, this._hiddenColumns, this._columnSizes, sortState);
                this.update();
            }
        }
        catch (error) {
            await state.remove(key);
        }
    }
    /**
     * Save the current column sizes and sort state to the state database.
     */
    _saveState() {
        if (this._state && this._stateColumnsKey) {
            void this._state.save(this._stateColumnsKey, {
                sizes: this._columnSizes,
                sortState: { ...this._sortState }
            });
        }
    }
    /**
     * Shut down kernels on the applicable currently selected items.
     *
     * @returns A promise that resolves when the operation is complete.
     */
    shutdownKernels() {
        const model = this._model;
        const items = this._sortedItems;
        const paths = items.map(item => item.path);
        const promises = Array.from(this._model.sessions())
            .filter(session => {
            const index = ArrayExt.firstIndexOf(paths, session.path);
            return this.selection[items[index].path];
        })
            .map(session => model.manager.services.sessions.shutdown(session.id));
        return Promise.all(promises)
            .then(() => {
            return undefined;
        })
            .catch(error => {
            void showErrorMessage(this._trans._p('showErrorMessage', 'Shut down kernel'), error);
        });
    }
    /**
     * Select next item.
     *
     * @param keepExisting - Whether to keep the current selection and add to it.
     */
    selectNext(keepExisting = false) {
        let index = -1;
        const selected = Object.keys(this.selection);
        const items = this._sortedItems;
        if (selected.length === 1 || keepExisting) {
            // Select the next item.
            const path = selected[selected.length - 1];
            index = ArrayExt.findFirstIndex(items, value => value.path === path);
            index += 1;
            if (index === this._items.length) {
                index = 0;
            }
        }
        else if (selected.length === 0) {
            // Select the first item.
            index = 0;
        }
        else {
            // Select the last selected item.
            const path = selected[selected.length - 1];
            index = ArrayExt.findFirstIndex(items, value => value.path === path);
        }
        if (index !== -1) {
            this._selectItem(index, keepExisting);
            ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
        }
    }
    /**
     * Select previous item.
     *
     * @param keepExisting - Whether to keep the current selection and add to it.
     */
    selectPrevious(keepExisting = false) {
        let index = -1;
        const selected = Object.keys(this.selection);
        const items = this._sortedItems;
        if (selected.length === 1 || keepExisting) {
            // Select the previous item.
            const path = selected[0];
            index = ArrayExt.findFirstIndex(items, value => value.path === path);
            index -= 1;
            if (index === -1) {
                index = this._items.length - 1;
            }
        }
        else if (selected.length === 0) {
            // Select the last item.
            index = this._items.length - 1;
        }
        else {
            // Select the first selected item.
            const path = selected[0];
            index = ArrayExt.findFirstIndex(items, value => value.path === path);
        }
        if (index !== -1) {
            this._selectItem(index, keepExisting);
            ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
        }
    }
    /**
     * Select the first item that starts with prefix being typed.
     */
    selectByPrefix() {
        const prefix = this._searchPrefix.toLowerCase();
        const items = this._sortedItems;
        const index = ArrayExt.findFirstIndex(items, value => {
            return value.name.toLowerCase().substr(0, prefix.length) === prefix;
        });
        if (index !== -1) {
            this._selectItem(index, false);
            ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
        }
    }
    /**
     * Get whether an item is selected by name.
     *
     * @param name - The name of of the item.
     *
     * @returns Whether the item is selected.
     */
    isSelected(name) {
        const items = this._sortedItems;
        return (Array.from(filter(items, item => item.name === name && this.selection[item.path])).length !== 0);
    }
    /**
     * Find a model given a click.
     *
     * @param event - The mouse event.
     *
     * @returns The model for the selected file.
     */
    modelForClick(event) {
        const items = this._sortedItems;
        const index = Private.hitTestNodes(this._items, event);
        if (index !== -1) {
            return items[index];
        }
        return undefined;
    }
    /**
     * Clear the selected items.
     */
    clearSelectedItems(emit = true) {
        this.selection = Object.create(null);
        if (emit) {
            this._selectionChanged.emit();
        }
    }
    /**
     * Move focus to selected item, or to the first item if no item is selected.
     */
    _focusContent() {
        const items = this._sortedItems;
        if (items.length === 0) {
            this._focusItem(0);
            return;
        }
        let index = this._focusPath !== ''
            ? ArrayExt.findFirstIndex(items, value => value.path === this._focusPath)
            : -1;
        // Fallbacks if path is missing/not found.
        if (index === -1) {
            index = Math.min(Math.max(this._focusIndex, 0), items.length - 1);
        }
        this._selectItem(index, false, true);
    }
    /**
     * Handle `'activate-request'` messages.
     */
    onActivateRequest(msg) {
        this._focusContent();
    }
    /**
     * Select an item by name.
     *
     * @param name - The name of the item to select.
     * @param focus - Whether to move focus to the selected item.
     *
     * @returns A promise that resolves when the name is selected.
     */
    async selectItemByName(name, focus = false) {
        return this._selectItemByName(name, focus);
    }
    /**
     * Select an item by name.
     *
     * @param name - The name of the item to select.
     * @param focus - Whether to move focus to the selected item.
     * @param force - Whether to proceed with selection even if the file was already selected.
     *
     * @returns A promise that resolves when the name is selected.
     */
    async _selectItemByName(name, focus = false, force = false) {
        if (!force && this.isSelected(name)) {
            // Avoid API polling and DOM updates if already selected
            return;
        }
        // Make sure the file is available.
        await this.model.refresh();
        if (this.isDisposed) {
            throw new Error('File browser is disposed.');
        }
        const items = this._sortedItems;
        const index = ArrayExt.findFirstIndex(items, value => value.name === name);
        if (index === -1) {
            throw new Error('Item does not exist.');
        }
        this._selectItem(index, false, focus);
        MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
        ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
    }
    /**
     * Handle the DOM events for the directory listing.
     *
     * @param event - The DOM event sent to the widget.
     *
     * #### Notes
     * This method implements the DOM `EventListener` interface and is
     * called in response to events on the panel's DOM node. It should
     * not be called directly by user code.
     */
    handleEvent(event) {
        switch (event.type) {
            case 'mousedown':
                this._evtMousedown(event);
                break;
            case 'mouseup':
                this._evtMouseup(event);
                break;
            case 'mousemove':
                this._evtMousemove(event);
                break;
            case 'keydown':
                this.evtKeydown(event);
                break;
            case 'click':
                this._evtClick(event);
                break;
            case 'dblclick':
                this.evtDblClick(event);
                break;
            case 'dragenter':
            case 'dragover':
                // Only show visual feedback if drag and drop upload is enabled
                if (this._allowDragDropUpload) {
                    this.addClass('jp-mod-native-drop');
                }
                event.preventDefault();
                break;
            case 'dragleave':
            case 'dragend':
                this.removeClass('jp-mod-native-drop');
                break;
            case 'drop':
                this.removeClass('jp-mod-native-drop');
                this.evtNativeDrop(event);
                break;
            case 'scroll':
                this._evtScroll(event);
                break;
            case 'lm-dragenter':
                this.evtDragEnter(event);
                break;
            case 'lm-dragleave':
                this.evtDragLeave(event);
                break;
            case 'lm-dragover':
                this.evtDragOver(event);
                break;
            case 'lm-drop':
                this.evtDrop(event);
                break;
            default:
                break;
        }
    }
    /**
     * A message handler invoked on an `'after-attach'` message.
     */
    onAfterAttach(msg) {
        super.onAfterAttach(msg);
        const node = this.node;
        this._width = this._computeContentWidth();
        const content = DOMUtils.findElement(node, CONTENT_CLASS);
        node.addEventListener('mousedown', this);
        node.addEventListener('keydown', this);
        node.addEventListener('click', this);
        node.addEventListener('dblclick', this);
        this._contentSizeObserver.observe(content);
        content.addEventListener('dragenter', this);
        content.addEventListener('dragover', this);
        content.addEventListener('dragleave', this);
        content.addEventListener('dragend', this);
        content.addEventListener('drop', this);
        content.addEventListener('scroll', this);
        content.addEventListener('lm-dragenter', this);
        content.addEventListener('lm-dragleave', this);
        content.addEventListener('lm-dragover', this);
        content.addEventListener('lm-drop', this);
        this._updateColumnSizes();
    }
    /**
     * A message handler invoked on a `'before-detach'` message.
     */
    onBeforeDetach(msg) {
        super.onBeforeDetach(msg);
        const node = this.node;
        const content = DOMUtils.findElement(node, CONTENT_CLASS);
        node.removeEventListener('mousedown', this);
        node.removeEventListener('keydown', this);
        node.removeEventListener('click', this);
        node.removeEventListener('dblclick', this);
        this._contentSizeObserver.disconnect();
        content.removeEventListener('scroll', this);
        content.removeEventListener('dragover', this);
        content.removeEventListener('dragover', this);
        content.removeEventListener('dragleave', this);
        content.removeEventListener('dragend', this);
        content.removeEventListener('drop', this);
        content.removeEventListener('lm-dragenter', this);
        content.removeEventListener('lm-dragleave', this);
        content.removeEventListener('lm-dragover', this);
        content.removeEventListener('lm-drop', this);
        document.removeEventListener('mousemove', this, true);
        document.removeEventListener('mouseup', this, true);
    }
    /**
     * A message handler invoked on an `'after-show'` message.
     */
    onAfterShow(msg) {
        if (this._isDirty) {
            // Update the sorted items.
            this.sort(this.sortState);
            this.update();
        }
    }
    _onContentResize() {
        const content = DOMUtils.findElement(this.node, CONTENT_CLASS);
        const scrollbarWidth = content.offsetWidth - content.clientWidth;
        if (scrollbarWidth != this._contentScrollbarWidth) {
            this._contentScrollbarWidth = scrollbarWidth;
            this._width = this._computeContentWidth();
            this._updateColumnSizes();
        }
    }
    _computeContentWidth(width = null) {
        if (!width) {
            width = this.node.getBoundingClientRect().width;
        }
        this._paddingWidth = parseFloat(window
            .getComputedStyle(this.node)
            .getPropertyValue('--jp-dirlisting-padding-width'));
        const handle = this.node.querySelector(`.${RESIZE_HANDLE_CLASS}`);
        this._handleWidth = handle
            ? handle.getBoundingClientRect().width
            : DEFAULT_HANDLE_WIDTH;
        return width - this._paddingWidth * 2 - this._contentScrollbarWidth;
    }
    /**
     * Update the 'modified' column's size
     */
    _updateModifiedSize(node) {
        this._modifiedStyle = this._computeDateColumnStyle(node, MODIFIED_ID_CLASS, 'last_modified');
    }
    /**
     * Update the 'date created' column's size
     */
    _updateCreatedSize(node) {
        this._createdStyle = this._computeDateColumnStyle(node, CREATED_ID_CLASS, 'date_created');
    }
    /**
     * Compute the Intl.RelativeTimeFormat style for a date column based on
     * its pixel width: < 100px → 'narrow', 100–120px → 'short', > 120px → 'long'.
     */
    _computeDateColumnStyle(node, columnClassName, columnName) {
        var _a, _b;
        const columnElement = DOMUtils.findElement(node, columnClassName);
        const width = (_b = (_a = this._columnSizes[columnName]) !== null && _a !== void 0 ? _a : columnElement === null || columnElement === void 0 ? void 0 : columnElement.getBoundingClientRect().width) !== null && _b !== void 0 ? _b : 83;
        return width < 100 ? 'narrow' : width > 120 ? 'long' : 'short';
    }
    /**
     * Rerender item nodes' modified dates, if the modified style has changed.
     */
    _updateModifiedStyleAndSize() {
        const oldModifiedStyle = this._modifiedStyle;
        this._updateModifiedSize(this.node);
        if (oldModifiedStyle !== this._modifiedStyle) {
            this.updateModified(this._sortedItems, this._items);
        }
    }
    /**
     * Rerender item nodes' created dates, if the created style has changed.
     */
    _updateCreatedStyleAndSize() {
        const oldCreatedStyle = this._createdStyle;
        this._updateCreatedSize(this.node);
        if (oldCreatedStyle !== this._createdStyle) {
            this.updateCreated(this._sortedItems, this._items);
        }
    }
    /**
     * Update only the modified dates.
     */
    updateModified(items, nodes) {
        items.forEach((item, i) => {
            const node = nodes[i];
            if (node && item.last_modified) {
                const modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
                if (!modified) {
                    return;
                }
                if (this.renderer.updateItemModified !== undefined) {
                    this.renderer.updateItemModified(modified, item.last_modified, this._modifiedStyle);
                }
                else {
                    DirListing.defaultRenderer.updateItemModified(modified, item.last_modified, this._modifiedStyle);
                }
            }
        });
    }
    /**
     * Update only the created dates.
     */
    updateCreated(items, nodes) {
        items.forEach((item, i) => {
            const node = nodes[i];
            if (node && item.created) {
                const created = DOMUtils.findElement(node, ITEM_CREATED_CLASS);
                if (!created) {
                    return;
                }
                if (this.renderer.updateItemCreated !== undefined) {
                    this.renderer.updateItemCreated(created, item.created, this._createdStyle);
                }
                else {
                    DirListing.defaultRenderer.updateItemCreated(created, item.created, this._createdStyle);
                }
            }
        });
    }
    // Update item nodes based on widget state.
    updateNodes(items, nodes, sizeOnly = false) {
        var _a;
        items.forEach((item, i) => {
            const node = nodes[i];
            if (sizeOnly && this.renderer.updateItemSize) {
                if (!node) {
                    // short-circuit in case if node is not yet ready
                    return;
                }
                return this.renderer.updateItemSize(node, item, this._modifiedStyle, this._columnSizes, this._createdStyle);
            }
            const ft = this._manager.registry.getFileTypeForModel(item);
            this.renderer.updateItemNode(node, item, ft, this.translator, this._hiddenColumns, this.selection[item.path], this._modifiedStyle, this._columnSizes, this._createdStyle);
            if (this.selection[item.path] &&
                this._isCut &&
                this._model.path === this._prevPath) {
                node.classList.add(CUT_CLASS);
            }
            // add metadata to the node
            node.setAttribute('data-isdir', item.type === 'directory' ? 'true' : 'false');
        });
        // Handle the selectors on the widget node.
        const selected = Object.keys(this.selection).length;
        if (selected) {
            this.addClass(SELECTED_CLASS);
            if (selected > 1) {
                this.addClass(MULTI_SELECTED_CLASS);
            }
        }
        // Handle file session statuses.
        const paths = items.map(item => item.path);
        for (const session of this._model.sessions()) {
            const index = ArrayExt.firstIndexOf(paths, session.path);
            const node = nodes[index];
            // Node may have been filtered out.
            if (node) {
                let name = (_a = session.kernel) === null || _a === void 0 ? void 0 : _a.name;
                const specs = this._model.specs;
                node.classList.add(RUNNING_CLASS);
                if (specs && name) {
                    const spec = specs.kernelspecs[name];
                    name = spec ? spec.display_name : this._trans.__('unknown');
                }
                // Update node title only if it has changed.
                const prevState = this._lastRenderedState.get(node);
                if (prevState !== node.title) {
                    node.title = this._trans.__('%1\nKernel: %2', node.title, name);
                    this._lastRenderedState.set(node, node.title);
                }
            }
        }
    }
    /**
     * A handler invoked on an `'update-request'` message.
     */
    onUpdateRequest(msg) {
        this._isDirty = false;
        // Fetch common variables.
        const items = this._sortedItems;
        const nodes = this._items;
        // If we didn't get any item yet, we'll need to resize once items are added
        const needsResize = nodes.length === 0;
        const content = DOMUtils.findElement(this.node, CONTENT_CLASS);
        const renderer = this._renderer;
        this.removeClass(MULTI_SELECTED_CLASS);
        this.removeClass(SELECTED_CLASS);
        // Remove any excess item nodes.
        while (nodes.length > items.length) {
            content.removeChild(nodes.pop());
        }
        // Add any missing item nodes.
        while (nodes.length < items.length) {
            const node = renderer.createItemNode(this._hiddenColumns, this._columnSizes);
            node.classList.add(ITEM_CLASS);
            nodes.push(node);
            content.appendChild(node);
        }
        nodes.forEach((node, i) => {
            // Remove extra classes from the nodes.
            node.classList.remove(SELECTED_CLASS);
            node.classList.remove(RUNNING_CLASS);
            node.classList.remove(CUT_CLASS);
            // Uncheck each file checkbox
            const checkbox = renderer.getCheckboxNode(node);
            if (checkbox) {
                checkbox.checked = false;
            }
            // Handle `tabIndex` & `role`
            const nameNode = renderer.getNameNode(node);
            if (nameNode) {
                // Must check if the name node is there because it gets replaced by the
                // edit node when editing the name of the file or directory.
                if (i === this._focusIndex) {
                    nameNode.setAttribute('tabIndex', '0');
                    nameNode.setAttribute('role', 'button');
                }
                else {
                    nameNode.setAttribute('tabIndex', '-1');
                    nameNode.removeAttribute('role');
                }
            }
        });
        // Put the check-all checkbox in the header into the correct state
        const checkAllCheckbox = renderer.getCheckboxNode(this.headerNode);
        if (checkAllCheckbox) {
            const totalSelected = Object.keys(this.selection).length;
            const allSelected = items.length > 0 && totalSelected === items.length;
            const someSelected = !allSelected && totalSelected > 0;
            checkAllCheckbox.checked = allSelected;
            checkAllCheckbox.indeterminate = someSelected;
            // Stash the state in data attributes so we can access them in the click
            // handler (because in the click handler, checkbox.checked and
            // checkbox.indeterminate do not hold the previous value; they hold the
            // next value).
            checkAllCheckbox.dataset.checked = String(allSelected);
            checkAllCheckbox.dataset.indeterminate = String(someSelected);
            const trans = this.translator.load('jupyterlab');
            checkAllCheckbox === null || checkAllCheckbox === void 0 ? void 0 : checkAllCheckbox.setAttribute('aria-label', allSelected || someSelected
                ? trans.__('Deselect all files and directories')
                : trans.__('Select all files and directories'));
        }
        this.updateNodes(items, nodes);
        if (needsResize) {
            this._width = this._computeContentWidth();
            this._updateColumnSizes();
        }
        this._prevPath = this._model.path;
    }
    onResize(msg) {
        const { width } = msg.width === -1 ? this.node.getBoundingClientRect() : msg;
        this._width = this._computeContentWidth(width);
        this._updateColumnSizes();
    }
    setColumnVisibility(name, visible) {
        if (visible) {
            this._hiddenColumns.delete(name);
        }
        else {
            this._hiddenColumns.add(name);
        }
        this.headerNode.innerHTML = '';
        this._renderer.populateHeaderNode(this.headerNode, this.translator, this._hiddenColumns, this._columnSizes, this._sortState);
        this._updateColumnSizes();
    }
    _updateColumnSizes(doNotGrowBeforeInclusive = null) {
        // Adjust column sizes so that they add up to the total width available, preserving ratios
        const visibleColumns = this._visibleColumns
            .map(column => ({
            ...column,
            element: DOMUtils.findElement(this.node, column.className)
        }))
            .filter(column => {
            // While all visible column will have an element, some extensions like jupyter-unfold
            // do not render columns even if user requests them to be visible; this filter exists
            // to ensure backward compatibility with such extensions and may be removed in the future.
            return column.element;
        });
        // read from DOM
        let total = 0;
        for (const column of visibleColumns) {
            let size = this._columnSizes[column.id];
            if (size === null) {
                size = column.element.getBoundingClientRect().width;
            }
            // restrict the minimum and maximum width
            size = Math.max(size, column.minWidth);
            if (this._width) {
                let reservedForOtherColumns = 0;
                for (const other of visibleColumns) {
                    if (other.id === column.id) {
                        continue;
                    }
                    reservedForOtherColumns += other.minWidth;
                }
                size = Math.min(size, this._width - reservedForOtherColumns);
            }
            this._columnSizes[column.id] = size;
            total += size;
        }
        // Ensure that total fits
        if (this._width) {
            // Distribute the excess/shortfall over the columns which should stretch.
            const excess = this._width - total;
            let growAllowed = doNotGrowBeforeInclusive === null;
            const growColumns = visibleColumns.filter(c => {
                if (growAllowed) {
                    return true;
                }
                if (c.id === doNotGrowBeforeInclusive) {
                    growAllowed = true;
                }
                return false;
            });
            const totalWeight = growColumns
                .map(c => c.grow)
                .reduce((a, b) => a + b, 0);
            for (const column of growColumns) {
                // The value of `growBy` will be negative when the down-sizing
                const growBy = (excess * column.grow) / totalWeight;
                this._columnSizes[column.id] = this._columnSizes[column.id] + growBy;
            }
        }
        const resizeHandles = this.node.getElementsByClassName(RESIZE_HANDLE_CLASS);
        const resizableColumns = visibleColumns.map(column => Private.isResizable(column));
        // Write to DOM
        let i = 0;
        for (const column of visibleColumns) {
            let size = this._columnSizes[column.id];
            if (Private.isResizable(column) && size) {
                size -=
                    (this._handleWidth * resizeHandles.length) / resizableColumns.length;
                // if this is first resizable or last resizable column
                if (i === 0 || i === resizableColumns.length - 1) {
                    size += this._paddingWidth;
                }
                i += 1;
            }
            column.element.style.width = size === null ? '' : size + 'px';
        }
        this._updateModifiedStyleAndSize();
        this._updateCreatedStyleAndSize();
        // Refresh sizes on the per item widths
        if (this.isVisible) {
            const items = this._items;
            if (items.length !== 0) {
                this.updateNodes(this._sortedItems, this._items, true);
            }
        }
        this._saveState();
    }
    get _visibleColumns() {
        return DirListing.columns.filter(column => { var _a; return column.id === 'name' || !((_a = this._hiddenColumns) === null || _a === void 0 ? void 0 : _a.has(column.id)); });
    }
    _setColumnSize(name, size) {
        var _a;
        const previousSize = this._columnSizes[name];
        if (previousSize && size && size > previousSize) {
            // check if we can resize up
            let total = 0;
            let before = true;
            for (const column of this._visibleColumns) {
                if (column.id === name) {
                    // add proposed size for the current columns
                    total += size;
                    before = false;
                    continue;
                }
                if (before) {
                    // add size as-is for columns before
                    const element = DOMUtils.findElement(this.node, column.className);
                    total +=
                        (_a = this._columnSizes[column.id]) !== null && _a !== void 0 ? _a : element.getBoundingClientRect().width;
                }
                else {
                    // add minimum acceptable size for columns after
                    total += column.minWidth;
                }
            }
            if (this._width && total > this._width) {
                // up sizing is no longer possible
                return;
            }
        }
        this._columnSizes[name] = size;
        this._updateColumnSizes(name);
    }
    /**
     * Update the setting to sort notebooks above files.
     * This sorts the items again if the internal value is modified.
     */
    setNotebooksFirstSorting(isEnabled) {
        let previousValue = this._sortNotebooksFirst;
        this._sortNotebooksFirst = isEnabled;
        if (this._sortNotebooksFirst !== previousValue) {
            this.sort(this._sortState);
        }
    }
    /**
     * Update the setting to sort file names naturally
     * vs lexicographically. Default is true (natural).
     * This sorts the items again if the internal value is modified.
     */
    setSortFileNamesNaturally(natural) {
        const previousValue = this._sortFileNamesNaturally;
        this._sortFileNamesNaturally = natural;
        if (this._sortFileNamesNaturally !== previousValue) {
            this.sort(this._sortState);
        }
    }
    /**
     * Update the setting to allow single click navigation.
     * This enables opening files/directories with a single click.
     */
    setAllowSingleClickNavigation(isEnabled) {
        this._allowSingleClick = isEnabled;
    }
    /**
     * Update the setting to allow drag and drop upload.
     * This enables uploading files via drag and drop.
     */
    setAllowDragDropUpload(isEnabled) {
        this._allowDragDropUpload = isEnabled;
    }
    /**
     * Would this click (or other event type) hit the checkbox by default?
     */
    isWithinCheckboxHitArea(event) {
        let element = event.target;
        while (element) {
            if (element.classList.contains(CHECKBOX_WRAPPER_CLASS)) {
                return true;
            }
            element = element.parentElement;
        }
        return false;
    }
    /**
     * Handle the `'click'` event for the widget.
     */
    _evtClick(event) {
        const target = event.target;
        const header = this.headerNode;
        const renderer = this._renderer;
        if (header.contains(target)) {
            const checkbox = renderer.getCheckboxNode(header);
            if (checkbox && this.isWithinCheckboxHitArea(event)) {
                const previouslyUnchecked = checkbox.dataset.indeterminate === 'false' &&
                    checkbox.dataset.checked === 'false';
                // The only time a click on the check-all checkbox should check all is
                // when it was previously unchecked; otherwise, if the checkbox was
                // either checked (all selected) or indeterminate (some selected), the
                // click should clear all.
                if (previouslyUnchecked) {
                    // Select all items
                    this._sortedItems.forEach((item) => (this.selection[item.path] = true));
                    this._selectionChanged.emit();
                }
                else {
                    // Unselect all items
                    this.clearSelectedItems();
                }
                this.update();
            }
            else {
                const state = this.renderer.handleHeaderClick(header, event);
                if (state) {
                    this.sort(state);
                }
            }
            return;
        }
        else {
            // Focus the selected file on click to ensure a couple of things:
            // 1. If a user clicks on the item node, its name node will receive focus.
            // 2. If a user clicks on blank space in the directory listing, the
            //    previously focussed item will be focussed.
            this._focusItem(this._focusIndex);
        }
        if (this._allowSingleClick) {
            this.evtDblClick(event);
        }
    }
    /**
     * Handle the `'scroll'` event for the widget.
     */
    _evtScroll(event) {
        this.headerNode.scrollLeft = this.contentNode.scrollLeft;
    }
    /**
     * Handle the `'mousedown'` event for the widget.
     */
    _evtMousedown(event) {
        // Bail if clicking within the edit node
        if (event.target === this._editNode) {
            return;
        }
        // Blur the edit node if necessary.
        if (this._editNode.parentNode) {
            if (this._editNode !== event.target) {
                this._editNode.focus();
                this._editNode.blur();
                clearTimeout(this._selectTimer);
            }
            else {
                return;
            }
        }
        let index = Private.hitTestNodes(this._items, event);
        if (index === -1) {
            // Left mouse press for drag or resize start.
            if (event.button === 0) {
                const resizeHandle = event.target;
                if (resizeHandle instanceof HTMLElement &&
                    resizeHandle.classList.contains(RESIZE_HANDLE_CLASS)) {
                    const columnId = resizeHandle.dataset.column;
                    if (!columnId) {
                        throw Error('Column resize handle is missing data-column attribute');
                    }
                    const column = DirListing.columns.find(c => c.id === columnId);
                    if (!column) {
                        throw Error(`Column with identifier ${columnId} not found`);
                    }
                    const element = DOMUtils.findElement(this.node, column.className);
                    resizeHandle.classList.add(ACTIVE_CLASS);
                    const cursorOverride = Drag.overrideCursor('col-resize');
                    this._resizeData = {
                        pressX: event.clientX,
                        column: columnId,
                        initialSize: element.getBoundingClientRect().width,
                        overrides: new DisposableDelegate(() => {
                            cursorOverride.dispose();
                            resizeHandle.classList.remove(ACTIVE_CLASS);
                        })
                    };
                    document.addEventListener('mouseup', this, true);
                    document.addEventListener('mousemove', this, true);
                    return;
                }
            }
            return;
        }
        this.handleFileSelect(event);
        if (event.button !== 0) {
            clearTimeout(this._selectTimer);
        }
        // Check for clearing a context menu.
        const newContext = (IS_MAC && event.ctrlKey) || event.button === 2;
        if (newContext) {
            return;
        }
        // Left mouse press for drag or resize start.
        if (event.button === 0) {
            this._dragData = {
                pressX: event.clientX,
                pressY: event.clientY,
                index: index
            };
            document.addEventListener('mouseup', this, true);
            document.addEventListener('mousemove', this, true);
        }
    }
    /**
     * Handle the `'mouseup'` event for the widget.
     */
    _evtMouseup(event) {
        // Handle any soft selection from the previous mouse down.
        if (this._softSelection) {
            const altered = event.metaKey || event.shiftKey || event.ctrlKey;
            // See if we need to clear the other selection.
            if (!altered && event.button === 0) {
                this.clearSelectedItems();
                this.selection[this._softSelection] = true;
                this._selectionChanged.emit();
                this.update();
            }
            this._softSelection = '';
        }
        // Re-focus. This is needed because nodes corresponding to files selected in
        // mousedown handler will not retain the focus as mousedown event is always
        // followed by a blur/focus event.
        if (event.button === 0) {
            this._focusItem(this._focusIndex);
        }
        // Remove the resize listeners if necessary.
        if (this._resizeData) {
            this._resizeData.overrides.dispose();
            this._resizeData = null;
            document.removeEventListener('mousemove', this, true);
            document.removeEventListener('mouseup', this, true);
            return;
        }
        // Remove the drag listeners if necessary.
        if (event.button !== 0 || !this._drag) {
            document.removeEventListener('mousemove', this, true);
            document.removeEventListener('mouseup', this, true);
            return;
        }
        event.preventDefault();
        event.stopPropagation();
    }
    /**
     * Handle the `'mousemove'` event for the widget.
     */
    _evtMousemove(event) {
        event.preventDefault();
        event.stopPropagation();
        if (this._resizeData) {
            const { initialSize, column, pressX } = this._resizeData;
            this._setColumnSize(column, initialSize + event.clientX - pressX);
            return;
        }
        // Bail if we are the one dragging.
        if (this._drag || !this._dragData) {
            return;
        }
        // Check for a drag initialization.
        const data = this._dragData;
        const dx = Math.abs(event.clientX - data.pressX);
        const dy = Math.abs(event.clientY - data.pressY);
        if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
            return;
        }
        this._startDrag(data.index, event.clientX, event.clientY);
    }
    /**
     * Handle the opening of an item.
     */
    handleOpen(item) {
        this._onItemOpened.emit(item);
        if (item.type === 'directory') {
            const localPath = this._manager.services.contents.localPath(item.path);
            this._model
                .cd(`/${localPath}`)
                .catch(error => showErrorMessage(this._trans._p('showErrorMessage', 'Open directory'), error));
        }
        else {
            const path = item.path;
            this._handleOpenFile(path);
        }
    }
    /**
     * Calculate the next focus index, given the current focus index and a
     * direction, keeping within the bounds of the directory listing.
     *
     * @param index Current focus index
     * @param direction -1 (up) or 1 (down)
     * @returns The next focus index, which could be the same as the current focus
     * index if at the boundary.
     */
    _getNextFocusIndex(index, direction) {
        const nextIndex = index + direction;
        if (nextIndex === -1 || nextIndex === this._items.length) {
            // keep focus index within bounds
            return index;
        }
        else {
            return nextIndex;
        }
    }
    /**
     * Handle the up or down arrow key.
     *
     * @param event The keyboard event
     * @param direction -1 (up) or 1 (down)
     */
    _handleArrowY(event, direction) {
        // We only handle the `ctrl` and `shift` modifiers. If other modifiers are
        // present, then do nothing.
        if (event.altKey || event.metaKey) {
            return;
        }
        // If folder is empty, there's nothing to do with the up/down key.
        if (!this._items.length) {
            return;
        }
        // Don't handle the arrow key press if it's not on directory item. This
        // avoids a confusing user experience that can result from when the user
        // moves the selection and focus index apart (via ctrl + up/down). The last
        // selected item remains highlighted but the last focussed item loses its
        // focus ring if it's not actively focussed.  This forces the user to
        // visibly reveal the last focussed item before moving the focus.
        if (!event.target.classList.contains(ITEM_TEXT_CLASS)) {
            return;
        }
        event.stopPropagation();
        event.preventDefault();
        const focusIndex = this._focusIndex;
        let nextFocusIndex = this._getNextFocusIndex(focusIndex, direction);
        // The following if-block allows the first press of the down arrow to select
        // the first (rather than the second) file/directory in the list. This is
        // the situation when the page first loads or when a user changes directory.
        if (direction > 0 &&
            focusIndex === 0 &&
            !event.ctrlKey &&
            Object.keys(this.selection).length === 0) {
            nextFocusIndex = 0;
        }
        // Shift key indicates multi-selection. Either the user is trying to grow
        // the selection, or shrink it.
        if (event.shiftKey) {
            this._handleMultiSelect(nextFocusIndex);
        }
        else if (!event.ctrlKey) {
            // If neither the shift nor ctrl keys were used with the up/down arrow,
            // then we treat it as a normal, unmodified key press and select the
            // next item.
            this._selectItem(nextFocusIndex, event.shiftKey, false /* focus = false because we call focus method directly following this */);
        }
        this._focusItem(nextFocusIndex);
        this.update();
    }
    /**
     * cd ..
     *
     * Go up one level in the directory tree.
     */
    async goUp() {
        const model = this.model;
        if (model.path === model.rootPath) {
            return;
        }
        // Check if we're at the navigation root boundary
        if (model.root && model.path === model.root) {
            return;
        }
        try {
            await model.cd('..');
        }
        catch (reason) {
            console.warn(`Failed to go to parent directory of ${model.path}`, reason);
        }
    }
    /**
     * Handle the `'keydown'` event for the widget.
     */
    evtKeydown(event) {
        // Do not handle any keydown events here if in the middle of a file rename.
        if (this._inRename) {
            return;
        }
        switch (event.key) {
            case 'Enter': {
                // Do nothing if any modifier keys are pressed.
                if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
                    return;
                }
                event.preventDefault();
                event.stopPropagation();
                for (const item of this.selectedItems()) {
                    this.handleOpen(item);
                }
                return;
            }
            case 'ArrowUp':
                this._handleArrowY(event, -1);
                return;
            case 'ArrowDown':
                this._handleArrowY(event, 1);
                return;
            case ' ':
            case 'Spacebar': {
                if (event.ctrlKey) {
                    // Follow the Windows and Ubuntu convention: you must press `ctrl` +
                    // `space` in order to toggle whether an item is selected.
                    // However, do not handle if any other modifiers were pressed.
                    if (event.metaKey || event.shiftKey || event.altKey) {
                        return;
                    }
                    // Make sure the ctrl+space key stroke was on a valid, focussed target.
                    const node = this._items[this._focusIndex];
                    if (!(
                    // Event must have occurred within a node whose item can be toggled.
                    (node.contains(event.target) &&
                        // That node must also contain the currently focussed element.
                        node.contains(document.activeElement)))) {
                        return;
                    }
                    event.stopPropagation();
                    // Prevent default, otherwise the container will scroll.
                    event.preventDefault();
                    // Toggle item selected
                    const { path } = this._sortedItems[this._focusIndex];
                    if (this.selection[path]) {
                        delete this.selection[path];
                    }
                    else {
                        this.selection[path] = true;
                    }
                    this._selectionChanged.emit();
                    this.update();
                    // Key was handled, so return.
                    return;
                }
                break;
            }
        }
        // Detects printable characters typed by the user.
        // Not all browsers support .key, but it discharges us from reconstructing
        // characters from key codes.
        if (event.key !== undefined &&
            event.key.length === 1 &&
            // Don't gobble up the space key on the check-all checkbox (which the
            // browser treats as a click event).
            !((event.key === ' ' || event.key === 'Spacebar') &&
                event.target.type === 'checkbox')) {
            if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
                return;
            }
            this._searchPrefix += event.key;
            clearTimeout(this._searchPrefixTimer);
            this._searchPrefixTimer = window.setTimeout(() => {
                this._searchPrefix = '';
            }, PREFIX_APPEND_DURATION);
            this.selectByPrefix();
            event.stopPropagation();
            event.preventDefault();
        }
    }
    /**
     * Handle the `'dblclick'` event for the widget.
     */
    evtDblClick(event) {
        // Do nothing if it's not a left mouse press.
        if (event.button !== 0) {
            return;
        }
        // Do nothing if any modifier keys are pressed.
        if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
            return;
        }
        // Do nothing if the double click is on a checkbox. (Otherwise a rapid
        // check-uncheck on the checkbox will cause the adjacent file/folder to
        // open, which is probably not what the user intended.)
        if (this.isWithinCheckboxHitArea(event)) {
            return;
        }
        // Stop the event propagation.
        event.preventDefault();
        event.stopPropagation();
        clearTimeout(this._selectTimer);
        this._editNode.blur();
        // Find a valid double click target.
        const target = event.target;
        const i = ArrayExt.findFirstIndex(this._items, node => node.contains(target));
        if (i === -1) {
            return;
        }
        const item = this._sortedItems[i];
        this.handleOpen(item);
    }
    /**
     * Handle the `drop` event for the widget.
     */
    evtNativeDrop(event) {
        var _a, _b, _c;
        // Prevent navigation
        event.preventDefault();
        // Check if drag and drop upload is disabled
        if (!this._allowDragDropUpload) {
            return;
        }
        const items = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.items;
        if (!items) {
            // Fallback to simple upload of files (if any)
            const files = (_b = event.dataTransfer) === null || _b === void 0 ? void 0 : _b.files;
            if (!files || files.length === 0) {
                return;
            }
            const promises = [];
            for (const file of files) {
                const promise = this._model.upload(file);
                promises.push(promise);
            }
            Promise.all(promises)
                .then(() => this._allUploaded.emit())
                .catch(err => {
                console.error('Error while uploading files: ', err);
            });
            return;
        }
        const uploadEntry = async (entry, path) => {
            if (Private.isDirectoryEntry(entry)) {
                const dirPath = await Private.createDirectory(this._model.manager, path, entry.name);
                const directoryReader = entry.createReader();
                const allEntries = await Private.collectEntries(directoryReader);
                for (const childEntry of allEntries) {
                    await uploadEntry(childEntry, dirPath);
                }
            }
            else if (Private.isFileEntry(entry)) {
                const file = await Private.readFile(entry);
                await this._model.upload(file, path);
            }
        };
        const promises = [];
        for (const item of items) {
            const entry = Private.defensiveGetAsEntry(item);
            if (!entry) {
                continue;
            }
            const promise = uploadEntry(entry, (_c = this._model.path) !== null && _c !== void 0 ? _c : '/');
            promises.push(promise);
        }
        Promise.all(promises)
            .then(() => this._allUploaded.emit())
            .catch(err => {
            console.error('Error while uploading files: ', err);
        });
    }
    /**
     * Signal emitted on when all files were uploaded after native drag.
     */
    get allUploaded() {
        return this._allUploaded;
    }
    /**
     * Handle the `'lm-dragenter'` event for the widget.
     */
    evtDragEnter(event) {
        if (event.mimeData.hasData(CONTENTS_MIME)) {
            const index = Private.hitTestNodes(this._items, event);
            if (index === -1) {
                return;
            }
            const item = this._sortedItems[index];
            if (item.type !== 'directory' || this.selection[item.path]) {
                return;
            }
            const target = event.target;
            target.classList.add(DROP_TARGET_CLASS);
            event.preventDefault();
            event.stopPropagation();
        }
    }
    /**
     * Handle the `'lm-dragleave'` event for the widget.
     */
    evtDragLeave(event) {
        event.preventDefault();
        event.stopPropagation();
        const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
        if (dropTarget) {
            dropTarget.classList.remove(DROP_TARGET_CLASS);
        }
    }
    /**
     * Handle the `'lm-dragover'` event for the widget.
     */
    evtDragOver(event) {
        event.preventDefault();
        event.stopPropagation();
        event.dropAction = event.proposedAction;
        const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
        if (dropTarget) {
            dropTarget.classList.remove(DROP_TARGET_CLASS);
        }
        const index = Private.hitTestNodes(this._items, event);
        if (index === -1 || index >= this._items.length) {
            event.dropAction = 'none';
            return;
        }
        this._items[index].classList.add(DROP_TARGET_CLASS);
    }
    /**
     * Handle the `'lm-drop'` event for the widget.
     */
    evtDrop(event) {
        event.preventDefault();
        event.stopPropagation();
        clearTimeout(this._selectTimer);
        if (event.proposedAction === 'none') {
            event.dropAction = 'none';
            return;
        }
        if (!event.mimeData.hasData(CONTENTS_MIME)) {
            return;
        }
        let target = event.target;
        while (target && target.parentElement) {
            if (target.classList.contains(DROP_TARGET_CLASS)) {
                target.classList.remove(DROP_TARGET_CLASS);
                break;
            }
            target = target.parentElement;
        }
        // Get the path based on the target node.
        const index = ArrayExt.firstIndexOf(this._items, target);
        const items = this._sortedItems;
        let basePath = this._model.path;
        if (items[index].type === 'directory') {
            basePath = PathExt.join(basePath, items[index].name);
        }
        const manager = this._manager;
        // Handle the items.
        const promises = [];
        const paths = event.mimeData.getData(CONTENTS_MIME);
        if (event.ctrlKey && event.proposedAction === 'move') {
            event.dropAction = 'copy';
        }
        else {
            event.dropAction = event.proposedAction;
        }
        for (const path of paths) {
            const localPath = manager.services.contents.localPath(path);
            const name = PathExt.basename(localPath);
            const newPath = PathExt.join(basePath, name);
            // Skip files that are not moving.
            if (newPath === path) {
                continue;
            }
            if (event.dropAction === 'copy') {
                promises.push(manager.copy(path, basePath));
            }
            else {
                promises.push(renameFile(manager, path, newPath));
            }
        }
        Promise.all(promises).catch(error => {
            void showErrorMessage(this._trans._p('showErrorMessage', 'Error while copying/moving files'), error);
        });
    }
    /**
     * Start a drag event.
     */
    _startDrag(index, clientX, clientY) {
        let selectedPaths = Object.keys(this.selection);
        const source = this._items[index];
        const items = this._sortedItems;
        let selectedItems;
        let item;
        // If the source node is not selected, use just that node.
        if (!source.classList.contains(SELECTED_CLASS)) {
            item = items[index];
            selectedPaths = [item.path];
            selectedItems = [item];
        }
        else {
            const path = selectedPaths[0];
            item = items.find(value => value.path === path);
            selectedItems = this.selectedItems();
        }
        if (!item) {
            return;
        }
        // Create the drag image.
        const ft = this._manager.registry.getFileTypeForModel(item);
        const dragImage = this.renderer.createDragImage(source, selectedPaths.length, this._trans, ft);
        // Set up the drag event.
        this._drag = new Drag({
            dragImage,
            mimeData: new MimeData(),
            supportedActions: 'move',
            proposedAction: 'move'
        });
        this._drag.mimeData.setData(CONTENTS_MIME, selectedPaths);
        // Add thunks for getting mime data content.
        // We thunk the content so we don't try to make a network call
        // when it's not needed. E.g. just moving files around
        // in a filebrowser
        const services = this.model.manager.services;
        for (const item of selectedItems) {
            this._drag.mimeData.setData(CONTENTS_MIME_RICH, {
                model: item,
                withContent: async () => {
                    return await services.contents.get(item.path);
                }
            });
        }
        if (item.type !== 'directory') {
            const otherPaths = selectedPaths.slice(1).reverse();
            this._drag.mimeData.setData(FACTORY_MIME, () => {
                if (!item) {
                    return;
                }
                const path = item.path;
                let widget = this._manager.findWidget(path);
                if (!widget) {
                    widget = this._manager.open(item.path);
                }
                if (otherPaths.length) {
                    const firstWidgetPlaced = new PromiseDelegate();
                    void firstWidgetPlaced.promise.then(() => {
                        let prevWidget = widget;
                        otherPaths.forEach(path => {
                            const options = {
                                ref: prevWidget === null || prevWidget === void 0 ? void 0 : prevWidget.id,
                                mode: 'tab-after'
                            };
                            prevWidget = this._manager.openOrReveal(path, void 0, void 0, options);
                            this._manager.openOrReveal(item.path);
                        });
                    });
                    firstWidgetPlaced.resolve(void 0);
                }
                return widget;
            });
        }
        // Start the drag and remove the mousemove and mouseup listeners.
        document.removeEventListener('mousemove', this, true);
        document.removeEventListener('mouseup', this, true);
        clearTimeout(this._selectTimer);
        void this._drag.start(clientX, clientY).then(action => {
            this._drag = null;
            clearTimeout(this._selectTimer);
        });
    }
    /**
     * Handle selection on a file node.
     */
    handleFileSelect(event) {
        // Fetch common variables.
        const items = this._sortedItems;
        const index = Private.hitTestNodes(this._items, event);
        clearTimeout(this._selectTimer);
        if (index === -1) {
            return;
        }
        // Clear any existing soft selection.
        this._softSelection = '';
        const path = items[index].path;
        const selected = Object.keys(this.selection);
        const isLeftClickOnCheckbox = event.button === 0 &&
            // On Mac, a left-click with the ctrlKey is treated as a right-click.
            !(IS_MAC && event.ctrlKey) &&
            this.isWithinCheckboxHitArea(event);
        // Handle toggling.
        if ((IS_MAC && event.metaKey) ||
            (!IS_MAC && event.ctrlKey) ||
            isLeftClickOnCheckbox) {
            if (this.selection[path]) {
                delete this.selection[path];
            }
            else {
                this.selection[path] = true;
            }
            this._selectionChanged.emit();
            this._focusItem(index);
            // Handle multiple select.
        }
        else if (event.shiftKey) {
            this._handleMultiSelect(index);
            this._focusItem(index);
            // Handle a 'soft' selection
        }
        else if (path in this.selection && selected.length > 1) {
            this._softSelection = path;
            // Default to selecting the only the item.
        }
        else {
            // Select only the given item.
            return this._selectItem(index, false, true);
        }
        this.update();
    }
    /**
     * (Re-)focus an item in the directory listing.
     *
     * @param index The index of the item node to focus
     */
    _focusItem(index) {
        var _a, _b;
        const items = this._items;
        if (items.length === 0) {
            // Focus the top node if the folder is empty and therefore there are no
            // items inside the folder to focus.
            this._focusIndex = 0;
            this._focusPath = '';
            this.node.focus();
            return;
        }
        const clampedIndex = Math.min(Math.max(index, 0), items.length - 1);
        this._focusIndex = clampedIndex;
        this._focusPath = (_b = (_a = this._sortedItems[clampedIndex]) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '';
        const node = items[clampedIndex];
        const nameNode = this.renderer.getNameNode(node);
        if (nameNode) {
            // Make the filename text node focusable so that it receives keyboard
            // events; text node was specifically chosen to receive shortcuts because
            // it gets substituted with input element during file name edits which
            // conveniently deactivates irrelevant shortcuts.
            nameNode.tabIndex = 0;
            nameNode.focus();
        }
    }
    /**
     * Are all of the items between two provided indices selected?
     *
     * The items at the indices are not considered.
     *
     * @param j Index of one item.
     * @param k Index of another item. Note: may be less or greater than first
     *          index.
     * @returns True if and only if all items between the j and k are selected.
     *          Returns undefined if j and k are the same.
     */
    _allSelectedBetween(j, k) {
        if (j === k) {
            return;
        }
        const [start, end] = j < k ? [j + 1, k] : [k + 1, j];
        return this._sortedItems
            .slice(start, end)
            .reduce((result, item) => result && this.selection[item.path], true);
    }
    /**
     * Handle a multiple select on a file item node.
     */
    _handleMultiSelect(index) {
        const items = this._sortedItems;
        const fromIndex = this._focusIndex;
        const target = items[index];
        let shouldAdd = true;
        if (index === fromIndex) {
            // This follows the convention in Ubuntu and Windows, which is to allow
            // the focussed item to gain but not lose selected status on shift-click.
            // (MacOS is irrelevant here because MacOS Finder has no notion of a
            // focused-but-not-selected state.)
            if (!this.selection[target.path]) {
                this.selection[target.path] = true;
                this._selectionChanged.emit();
            }
            return;
        }
        // If the target and all items in-between are selected, then we assume that
        // the user is trying to shrink rather than grow the group of selected
        // items.
        if (this.selection[target.path]) {
            // However, there is a special case when the distance between the from-
            // and to- index is just one (for example, when the user is pressing the
            // shift key plus arrow-up/down). If and only if the situation looks like
            // the following when going down (or reverse when going up) ...
            //
            // - [ante-anchor / previous item] unselected (or boundary)
            // - [anchor / currently focussed item / item at from-index] selected
            // - [target / next item / item at to-index] selected
            //
            // ... then we shrink the selection / unselect the currently focussed
            // item.
            if (Math.abs(index - fromIndex) === 1) {
                const anchor = items[fromIndex];
                const anteAnchor = items[fromIndex + (index < fromIndex ? 1 : -1)];
                if (
                // Currently focussed item is selected
                this.selection[anchor.path] &&
                    // Item on other side of focussed item (away from target) is either a
                    // boundary or unselected
                    (!anteAnchor || !this.selection[anteAnchor.path])) {
                    delete this.selection[anchor.path];
                    this._selectionChanged.emit();
                }
            }
            else if (this._allSelectedBetween(fromIndex, index)) {
                shouldAdd = false;
            }
        }
        // Select (or unselect) the rows between chosen index (target) and the last
        // focussed.
        const step = fromIndex < index ? 1 : -1;
        let selectionModified = false;
        for (let i = fromIndex; i !== index + step; i += step) {
            if (shouldAdd) {
                if (i === fromIndex) {
                    // Do not change the selection state of the starting (fromIndex) item.
                    continue;
                }
                if (!this.selection[items[i].path]) {
                    this.selection[items[i].path] = true;
                    selectionModified = true;
                }
            }
            else {
                if (i === index) {
                    // Do not unselect the target item.
                    continue;
                }
                if (this.selection[items[i].path]) {
                    delete this.selection[items[i].path];
                    selectionModified = true;
                }
            }
        }
        if (selectionModified) {
            this._selectionChanged.emit();
        }
    }
    /**
     * Copy the selected items, and optionally cut as well.
     */
    _copy() {
        this._clipboard.length = 0;
        for (const item of this.selectedItems()) {
            this._clipboard.push(item.path);
        }
    }
    /**
     * Delete the files with the given paths.
     */
    async _delete(paths) {
        await Promise.all(paths.map(path => this._model.manager.deleteFile(path).catch(err => {
            void showErrorMessage(this._trans._p('showErrorMessage', 'Delete Failed'), err);
        })));
    }
    /**
     * Allow the user to rename item on a given row.
     */
    async _doRename() {
        this._inRename = true;
        const selectedPaths = Object.keys(this.selection);
        // Bail out if nothing has been selected.
        if (selectedPaths.length === 0) {
            this._inRename = false;
            return Promise.resolve('');
        }
        // Figure out which selected path to use for the rename.
        const items = this._sortedItems;
        let { path } = items[this._focusIndex];
        if (!this.selection[path]) {
            // If the currently focused item is not selected, then choose the last
            // selected item.
            path = selectedPaths.slice(-1)[0];
        }
        // Get the corresponding model, nodes, and file name.
        const index = ArrayExt.findFirstIndex(items, value => value.path === path);
        const row = this._items[index];
        const item = items[index];
        const nameNode = this.renderer.getNameNode(row);
        const original = item.name;
        // Seed the text input with current file name, and select and focus it.
        this._editNode.value = original;
        this._selectItem(index, false, true);
        // Wait for user input
        const newName = await Private.userInputForRename(nameNode, this._editNode, original);
        // Check if the widget was disposed during the `await`.
        if (this.isDisposed) {
            this._inRename = false;
            throw new Error('File browser is disposed.');
        }
        let finalFilename = newName;
        if (!newName || newName === original) {
            finalFilename = original;
        }
        else if (!isValidFileName(newName)) {
            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)));
            finalFilename = original;
        }
        else {
            // Attempt rename at the file system level.
            const manager = this._manager;
            const oldPath = PathExt.join(this._model.path, original);
            const newPath = PathExt.join(this._model.path, newName);
            try {
                await renameFile(manager, oldPath, newPath);
            }
            catch (error) {
                if (error !== 'File not renamed') {
                    void showErrorMessage(this._trans._p('showErrorMessage', 'Rename Error'), error);
                }
                finalFilename = original;
            }
            // Check if the widget was disposed during the `await`.
            if (this.isDisposed) {
                this._inRename = false;
                throw new Error('File browser is disposed.');
            }
        }
        // If nothing else has been selected, then select the renamed file. In
        // other words, don't select the renamed file if the user has clicked
        // away to some other file.
        if (!this.isDisposed &&
            Object.keys(this.selection).length === 1 &&
            // We haven't updated the instance yet to reflect the rename, so unless
            // the user or something else has updated the selection, the original file
            // path and not the new file path will be in `this.selection`.
            this.selection[item.path]) {
            try {
                await this._selectItemByName(finalFilename, true, true);
            }
            catch (_a) {
                // do nothing
                console.warn('After rename, failed to select file', finalFilename);
            }
        }
        this._inRename = false;
        return finalFilename;
    }
    /**
     * Select a given item.
     */
    _selectItem(index, keepExisting, focus = true) {
        // Selected the given row(s)
        const items = this._sortedItems;
        if (!keepExisting) {
            this.clearSelectedItems();
        }
        const path = items[index].path;
        this.selection[path] = true;
        this._selectionChanged.emit();
        if (focus) {
            this._focusItem(index);
        }
        this.update();
    }
    /**
     * Handle the `refreshed` signal from the model.
     */
    _onModelRefreshed() {
        // Update the selection.
        const existing = Object.keys(this.selection);
        this.clearSelectedItems(false);
        for (const item of this._model.items()) {
            const path = item.path;
            if (existing.indexOf(path) !== -1) {
                this.selection[path] = true;
            }
        }
        if (this.isVisible) {
            // Update the sorted items.
            this.sort(this.sortState);
        }
        else {
            this._isDirty = true;
        }
    }
    /**
     * Handle a `pathChanged` signal from the model.
     */
    _onPathChanged() {
        // Check if the directory listing (or any of its children) has focus.
        const hasFocus = this.node.contains(document.activeElement);
        // Reset the selection.
        this.clearSelectedItems();
        // Update the sorted items.
        this.sort(this.sortState);
        // Reset focus. But wait until the DOM has been updated (hence
        // `requestAnimationFrame`).
        requestAnimationFrame(() => {
            if (this.isDisposed) {
                return;
            }
            // Only focus the first item if the listing (or a child) previously had focus.
            if (hasFocus) {
                this._focusItem(0);
            }
            else {
                // Otherwise, just reset the internal focus index without moving DOM focus.
                this._focusIndex = 0;
            }
        });
    }
    /**
     * Handle a `fileChanged` signal from the model.
     */
    _onFileChanged(sender, args) {
        const newValue = args.newValue;
        if (!newValue) {
            return;
        }
        const name = newValue.name;
        if (args.type !== 'new' || !name) {
            return;
        }
        void this.selectItemByName(name).catch(() => {
            /* Ignore if file does not exist. */
        });
    }
    /**
     * Handle an `activateRequested` signal from the manager.
     */
    _onActivateRequested(sender, args) {
        const dirname = PathExt.dirname(args);
        if (dirname !== this._model.path) {
            return;
        }
        const basename = PathExt.basename(args);
        this.selectItemByName(basename).catch(() => {
            /* Ignore if file does not exist. */
        });
    }
}
/**
 * The namespace for the `DirListing` class statics.
 */
(function (DirListing) {
    /**
     * Column definitions.
     */
    DirListing.columns = [
        {
            id: 'is_selected',
            className: CHECKBOX_WRAPPER_CLASS,
            itemClassName: CHECKBOX_WRAPPER_CLASS,
            minWidth: 18,
            resizable: false,
            sortable: false,
            grow: 0
        },
        {
            id: 'name',
            className: NAME_ID_CLASS,
            itemClassName: ITEM_NAME_COLUMN_CLASS,
            minWidth: 60,
            resizable: true,
            sortable: true,
            caretSide: 'right',
            grow: 3
        },
        {
            id: 'last_modified',
            className: MODIFIED_ID_CLASS,
            itemClassName: ITEM_MODIFIED_CLASS,
            minWidth: 60,
            resizable: true,
            sortable: true,
            caretSide: 'left',
            grow: 1
        },
        {
            id: 'date_created',
            className: CREATED_ID_CLASS,
            itemClassName: ITEM_CREATED_CLASS,
            minWidth: 60,
            resizable: true,
            sortable: true,
            caretSide: 'left',
            grow: 1
        },
        {
            id: 'file_size',
            className: FILE_SIZE_ID_CLASS,
            itemClassName: ITEM_FILE_SIZE_CLASS,
            minWidth: 60,
            resizable: true,
            sortable: true,
            caretSide: 'left',
            grow: 0.5
        }
    ];
    /**
     * The default implementation of an `IRenderer`.
     */
    class Renderer {
        constructor() {
            /**
             * Factories for individual parts of the item.
             */
            this.itemFactories = {
                name: () => {
                    const name = document.createElement('span');
                    const icon = document.createElement('span');
                    const text = document.createElement('span');
                    icon.className = ITEM_ICON_CLASS;
                    text.className = ITEM_TEXT_CLASS;
                    name.className = ITEM_NAME_COLUMN_CLASS;
                    name.appendChild(icon);
                    name.appendChild(text);
                    return name;
                },
                last_modified: () => {
                    const modified = document.createElement('span');
                    modified.className = ITEM_MODIFIED_CLASS;
                    return modified;
                },
                file_size: () => {
                    const fileSize = document.createElement('span');
                    fileSize.className = ITEM_FILE_SIZE_CLASS;
                    return fileSize;
                },
                date_created: () => {
                    const created = document.createElement('span');
                    created.className = ITEM_CREATED_CLASS;
                    return created;
                },
                is_selected: () => this.createCheckboxWrapperNode()
            };
            /**
             * Register of most recent arguments for last modified column update.
             */
            this._modifiedColumnLastUpdate = new WeakMap();
            /**
             * Register of most recent arguments for date created column update.
             */
            this._createdColumnLastUpdate = new WeakMap();
            this._lastRenderedState = new WeakMap();
        }
        /**
         * Create the DOM node for a dir listing.
         */
        createNode() {
            const node = document.createElement('div');
            const header = document.createElement('div');
            const content = document.createElement('ul');
            // Allow the node to scroll while dragging items.
            content.setAttribute('data-lm-dragscroll', 'true');
            content.className = CONTENT_CLASS;
            header.className = HEADER_CLASS;
            node.appendChild(header);
            node.appendChild(content);
            // Set to -1 to allow calling this.node.focus().
            node.tabIndex = -1;
            return node;
        }
        /**
         * Populate and empty header node for a dir listing.
         *
         * @param node - The header node to populate.
         */
        populateHeaderNode(node, translator, hiddenColumns, columnsSizes, sortState) {
            translator = translator || nullTranslator;
            const trans = translator.load('jupyterlab');
            const elementCreators = {
                name: () => this.createHeaderItemNode(trans.__('Name')),
                last_modified: () => this._createHeaderItemNodeWithSizes({
                    small: trans.__('Modified'),
                    large: trans.__('Last Modified')
                }),
                file_size: () => this._createHeaderItemNodeWithSizes({
                    small: trans.__('Size'),
                    large: trans.__('File Size')
                }),
                date_created: () => this._createHeaderItemNodeWithSizes({
                    small: trans.__('Created'),
                    large: trans.__('Date Created')
                }),
                is_selected: () => this.createCheckboxWrapperNode({
                    alwaysVisible: true,
                    headerNode: true
                })
            };
            const visibleColumns = DirListing.columns.filter(column => column.id === 'name' || !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has(column.id)));
            for (const column of visibleColumns) {
                const createElement = elementCreators[column.id];
                const element = createElement();
                element.classList.add(column.className);
                const isLastVisible = column.id === visibleColumns[visibleColumns.length - 1].id;
                if (columnsSizes) {
                    const size = columnsSizes[column.id];
                    if (!isLastVisible) {
                        element.style.width = size + 'px';
                    }
                }
                node.appendChild(element);
                if (Private.isResizable(column) && !isLastVisible) {
                    const resizer = document.createElement('div');
                    resizer.classList.add(RESIZE_HANDLE_CLASS);
                    resizer.dataset.column = column.id;
                    node.appendChild(resizer);
                }
            }
            this._updateSortIndicator(node, sortState !== null && sortState !== void 0 ? sortState : { direction: 'ascending', key: 'name' });
        }
        /**
         * Handle a header click.
         *
         * @param node - A node populated by [[populateHeaderNode]].
         *
         * @param event - A click event on the node.
         *
         * @returns The sort state of the header after the click event.
         */
        handleHeaderClick(node, event) {
            const target = event.target;
            const sortableColumns = DirListing.columns.filter(Private.isSortable);
            for (const column of sortableColumns) {
                const header = node.querySelector(`.${column.className}`);
                if (!header) {
                    continue;
                }
                if (header.contains(target)) {
                    const isSelected = header.classList.contains(SELECTED_CLASS);
                    const isDescending = header.classList.contains(DESCENDING_CLASS);
                    let direction = 'ascending';
                    if (isSelected && !isDescending) {
                        direction = 'descending';
                    }
                    const state = { direction, key: column.id };
                    this._updateSortIndicator(node, state);
                    return state;
                }
            }
            return { direction: 'ascending', key: 'name' };
        }
        _updateSortIndicator(node, sortState) {
            const sortableColumns = DirListing.columns.filter(Private.isSortable);
            for (const column of sortableColumns) {
                const header = node.querySelector(`.${column.className}`);
                if (!header) {
                    continue;
                }
                const headerIcon = DOMUtils.findElement(header, HEADER_ITEM_ICON_CLASS);
                if (column.id === sortState.key) {
                    header.classList.add(SELECTED_CLASS);
                    if (sortState.direction === 'descending') {
                        header.classList.add(DESCENDING_CLASS);
                        Private.updateCaret(headerIcon, column.caretSide, 'down');
                    }
                    else {
                        header.classList.remove(DESCENDING_CLASS);
                        Private.updateCaret(headerIcon, column.caretSide, 'up');
                    }
                }
                else {
                    header.classList.remove(SELECTED_CLASS);
                    header.classList.remove(DESCENDING_CLASS);
                    Private.updateCaret(headerIcon, column.caretSide);
                }
            }
        }
        /**
         * Create a new item node for a dir listing.
         *
         * @returns A new DOM node to use as a content item.
         */
        createItemNode(hiddenColumns, columnsSizes) {
            const node = document.createElement('li');
            for (const column of DirListing.columns) {
                if (column.id != 'name' && (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has(column.id))) {
                    continue;
                }
                const createElement = this.itemFactories[column.id];
                const element = createElement();
                node.appendChild(element);
                if (columnsSizes) {
                    const size = columnsSizes[column.id];
                    element.style.width = size + 'px';
                }
            }
            return node;
        }
        /**
         * Creates a node containing a checkbox.
         *
         * We wrap the checkbox in a label element in order to increase its hit
         * area. This is because the padding of the checkbox itself cannot be
         * increased via CSS, as the CSS/form compatibility table at the following
         * url from MDN shows:
         * https://developer.mozilla.org/en-US/docs/Learn/Forms/Property_compatibility_table_for_form_controls#check_boxes_and_radio_buttons
         *
         * @param [options]
         * @params options.alwaysVisible Should the checkbox be visible even when
         * not hovered?
         * @returns A new DOM node that contains a checkbox.
         */
        createCheckboxWrapperNode(options) {
            // Wrap the checkbox in a label element in order to increase its hit area.
            const labelWrapper = document.createElement('label');
            labelWrapper.classList.add(CHECKBOX_WRAPPER_CLASS);
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            // Prevent the user from clicking (via mouse, keyboard, or touch) the
            // checkbox since other code handles the mouse and keyboard events and
            // controls the checked state of the checkbox.
            if (!(options === null || options === void 0 ? void 0 : options.headerNode)) {
                checkbox.addEventListener('click', event => {
                    event.preventDefault();
                });
            }
            // The individual file checkboxes are visible on hover, but the header
            // check-all checkbox is always visible.
            if (options === null || options === void 0 ? void 0 : options.alwaysVisible) {
                labelWrapper.classList.add('jp-mod-visible');
            }
            else {
                // Disable tabbing to all other checkboxes.
                checkbox.tabIndex = -1;
            }
            labelWrapper.appendChild(checkbox);
            return labelWrapper;
        }
        /**
         * Update an item's last modified date.
         *
         * @param modified - Element containing the file's last modified date.
         *
         * @param modifiedDate - String representation of the last modified date.
         *
         * @param modifiedStyle - The date style for the modified column: narrow, short, or long
         */
        updateItemModified(modified, modifiedDate, modifiedStyle) {
            this._updateItemDate(modified, modifiedDate, modifiedStyle, this._modifiedColumnLastUpdate);
        }
        /**
         * Update an item's created date.
         *
         * @param created - Element containing the file's created date.
         *
         * @param createdDate - String representation of the created date.
         *
         * @param createdStyle - The date style for the created column: narrow, short, or long
         */
        updateItemCreated(created, createdDate, createdStyle) {
            this._updateItemDate(created, createdDate, createdStyle, this._createdColumnLastUpdate);
        }
        /**
         * Shared implementation for updating a date column element.
         *
         * Formatting dates is expensive (0.1-0.2ms per call, so over 150 files
         * can easily already choke the renderer), so we do the bare minimum check
         * of comparing if an update is needed using a last update cache.
         *
         * @param element - The date column element to update.
         *
         * @param dateString - String representation of the date.
         *
         * @param style - The date style: narrow, short, or long.
         *
         * @param cache - The WeakMap cache tracking the last rendered state for
         *   this column.
         */
        _updateItemDate(element, dateString, style, cache) {
            const previousUpdate = cache.get(element);
            if ((previousUpdate === null || previousUpdate === void 0 ? void 0 : previousUpdate.date) === dateString &&
                (previousUpdate === null || previousUpdate === void 0 ? void 0 : previousUpdate.style) === style) {
                return;
            }
            const parsedDate = new Date(dateString);
            // Render the date in one of multiple formats, depending on the container's size
            element.textContent = Time.formatHuman(parsedDate, style);
            element.title = Time.format(parsedDate);
            cache.set(element, { date: dateString, style });
        }
        /**
         * Update an item node to reflect the current state of a model.
         *
         * @param node - A node created by [[createItemNode]].
         *
         * @param model - The model object to use for the item state.
         *
         * @param fileType - The file type of the item, if applicable.
         *
         */
        updateItemNode(node, model, fileType, translator, hiddenColumns, selected, modifiedStyle, columnsSizes, createdStyle) {
            var _a;
            if (selected) {
                node.classList.add(SELECTED_CLASS);
            }
            fileType =
                fileType || DocumentRegistry.getDefaultTextFileType(translator);
            const { icon, iconClass, name } = fileType;
            translator = translator || nullTranslator;
            const trans = translator.load('jupyterlab');
            const prevState = this._lastRenderedState.get(node);
            const newState = JSON.stringify({
                name: model.name,
                selected,
                lastModified: model.last_modified,
                created: model.created,
                modifiedStyle,
                createdStyle,
                hiddenColumns: Array.from(hiddenColumns !== null && hiddenColumns !== void 0 ? hiddenColumns : []).sort(),
                columnsSizes,
                fileSize: model.size
            });
            const checkboxWrapper = DOMUtils.findElement(node, CHECKBOX_WRAPPER_CLASS);
            const checkbox = checkboxWrapper === null || checkboxWrapper === void 0 ? void 0 : checkboxWrapper.querySelector('input[type="checkbox"]');
            if (checkbox)
                checkbox.checked = selected !== null && selected !== void 0 ? selected : false;
            if (prevState === newState)
                return;
            this._lastRenderedState.set(node, newState);
            const iconContainer = DOMUtils.findElement(node, ITEM_ICON_CLASS);
            const text = DOMUtils.findElement(node, ITEM_TEXT_CLASS);
            const nameColumn = DOMUtils.findElement(node, ITEM_NAME_COLUMN_CLASS);
            let modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
            let fileSize = DOMUtils.findElement(node, ITEM_FILE_SIZE_CLASS);
            let created = DOMUtils.findElement(node, ITEM_CREATED_CLASS);
            const showFileCheckboxes = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'));
            if (checkboxWrapper && !showFileCheckboxes) {
                node.removeChild(checkboxWrapper);
            }
            else if (showFileCheckboxes && !checkboxWrapper) {
                const checkboxWrapper = this.createCheckboxWrapperNode();
                nameColumn.insertAdjacentElement('beforebegin', checkboxWrapper);
            }
            const showModified = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified'));
            if (modified && !showModified) {
                node.removeChild(modified);
                modified = undefined;
            }
            else if (showModified && !modified) {
                modified = this.itemFactories.last_modified();
                nameColumn.insertAdjacentElement('afterend', modified);
            }
            const showCreated = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('date_created'));
            if (created && !showCreated) {
                node.removeChild(created);
                created = undefined;
            }
            else if (showCreated && !created) {
                created = this.itemFactories.date_created();
                (modified !== null && modified !== void 0 ? modified : nameColumn).insertAdjacentElement('afterend', created);
            }
            const showFileSize = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size'));
            if (fileSize && !showFileSize) {
                node.removeChild(fileSize);
            }
            else if (showFileSize && !fileSize) {
                fileSize = this.itemFactories.file_size();
                const insertAfter = (_a = created !== null && created !== void 0 ? created : modified) !== null && _a !== void 0 ? _a : nameColumn;
                if (insertAfter) {
                    insertAfter.insertAdjacentElement('afterend', fileSize);
                }
            }
            // render the file item's icon
            requestAnimationFrame(() => {
                LabIcon.resolveElement({
                    icon,
                    iconClass: classes(iconClass, 'jp-Icon'),
                    container: iconContainer,
                    className: ITEM_ICON_CLASS,
                    stylesheet: 'listing'
                });
            });
            let hoverText = trans.__('Name: %1', model.name);
            // add file size to pop up if its available
            if (model.size !== null && model.size !== undefined) {
                const fileSizeText = formatFileSize(model.size, 1, 1024);
                if (fileSize) {
                    fileSize.textContent = fileSizeText;
                }
                hoverText += trans.__('\nSize: %1', formatFileSize(model.size, 1, 1024));
            }
            else if (fileSize) {
                fileSize.textContent = '';
            }
            if (model.path) {
                const dirname = PathExt.dirname(model.path);
                if (dirname) {
                    hoverText += trans.__('\nPath: %1', dirname.substr(0, 50));
                    if (dirname.length > 50) {
                        hoverText += '...';
                    }
                }
            }
            if (model.created) {
                hoverText += trans.__('\nCreated: %1', Time.format(new Date(model.created)));
            }
            if (model.last_modified) {
                hoverText += trans.__('\nModified: %1', Time.format(new Date(model.last_modified)));
            }
            hoverText += trans.__('\nWritable: %1', model.writable);
            node.title = hoverText;
            node.setAttribute('data-file-type', name);
            if (model.name.startsWith('.')) {
                node.setAttribute('data-is-dot', 'true');
            }
            else {
                node.removeAttribute('data-is-dot');
            }
            // If an item is being edited currently, its text node is unavailable.
            const indices = !model.indices ? [] : model.indices;
            let highlightedName = StringExt.highlight(model.name, indices, h.mark);
            if (text) {
                VirtualDOM.render(h.span(highlightedName), text);
            }
            // Adds an aria-label to the checkbox element.
            if (checkbox) {
                let ariaLabel;
                if (fileType.contentType === 'directory') {
                    ariaLabel = selected
                        ? trans.__('Deselect directory "%1"', highlightedName)
                        : trans.__('Select directory "%1"', highlightedName);
                }
                else {
                    ariaLabel = selected
                        ? trans.__('Deselect file "%1"', highlightedName)
                        : trans.__('Select file "%1"', highlightedName);
                }
                checkbox.setAttribute('aria-label', ariaLabel);
                checkbox.checked = selected !== null && selected !== void 0 ? selected : false;
            }
            this.updateItemSize(node, model, modifiedStyle, columnsSizes, createdStyle);
        }
        /**
         * Update size of item nodes, assuming that model has not changed.
         */
        updateItemSize(node, model, modifiedStyle, columnsSizes, createdStyle) {
            if (columnsSizes) {
                for (const column of DirListing.columns) {
                    const element = DOMUtils.findElement(node, column.itemClassName);
                    if (!element) {
                        continue;
                    }
                    const sizeSpec = columnsSizes[column.id];
                    const newWidth = sizeSpec === null ? '' : sizeSpec + 'px';
                    if (newWidth !== element.style.width) {
                        element.style.width = newWidth;
                    }
                }
            }
            let modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
            if (model.last_modified && modified) {
                this.updateItemModified(modified, model.last_modified, modifiedStyle !== null && modifiedStyle !== void 0 ? modifiedStyle : 'short');
            }
            let created = DOMUtils.findElement(node, ITEM_CREATED_CLASS);
            if (model.created && created) {
                this.updateItemCreated(created, model.created, createdStyle !== null && createdStyle !== void 0 ? createdStyle : 'short');
            }
        }
        /**
         * Get the node containing the file name.
         *
         * @param node - A node created by [[createItemNode]].
         *
         * @returns The node containing the file name.
         */
        getNameNode(node) {
            return DOMUtils.findElement(node, ITEM_TEXT_CLASS);
        }
        /**
         * Get the checkbox input element node.
         *
         * @param node A node created by [[createItemNode]] or
         * [[createHeaderItemNode]]
         *
         * @returns The checkbox node.
         */
        getCheckboxNode(node) {
            return node.querySelector(`.${CHECKBOX_WRAPPER_CLASS} input[type=checkbox]`);
        }
        /**
         * Create a drag image for an item.
         *
         * @param node - A node created by [[createItemNode]].
         *
         * @param count - The number of items being dragged.
         *
         * @param fileType - The file type of the item, if applicable.
         *
         * @returns An element to use as the drag image.
         */
        createDragImage(node, count, trans, fileType) {
            const dragImage = node.cloneNode(true);
            const icon = DOMUtils.findElement(dragImage, ITEM_ICON_CLASS);
            // Hide additional columns from the drag image to keep it unobtrusive.
            const extraColumns = DirListing.columns.filter(column => column.id !== 'name');
            for (const extraColumn of extraColumns) {
                const columnElement = DOMUtils.findElement(dragImage, extraColumn.itemClassName);
                if (!columnElement) {
                    // We can only remove columns which are rendered.
                    continue;
                }
                dragImage.removeChild(columnElement);
            }
            if (!fileType) {
                icon.textContent = '';
                icon.className = '';
            }
            else {
                icon.textContent = fileType.iconLabel || '';
                icon.className = fileType.iconClass || '';
            }
            icon.classList.add(DRAG_ICON_CLASS);
            if (count > 1) {
                const nameNode = DOMUtils.findElement(dragImage, ITEM_TEXT_CLASS);
                nameNode.textContent = trans._n('%1 Item', '%1 Items', count);
            }
            return dragImage;
        }
        /**
         * Create a node for a header item.
         */
        createHeaderItemNode(label) {
            const node = document.createElement('div');
            const text = document.createElement('span');
            const icon = document.createElement('span');
            node.className = HEADER_ITEM_CLASS;
            text.className = HEADER_ITEM_TEXT_CLASS;
            icon.className = HEADER_ITEM_ICON_CLASS;
            text.textContent = label;
            node.appendChild(text);
            node.appendChild(icon);
            return node;
        }
        /**
         * Create a node for a header item with multiple sizes.
         */
        _createHeaderItemNodeWithSizes(labels) {
            const node = document.createElement('div');
            node.className = HEADER_ITEM_CLASS;
            const icon = document.createElement('span');
            icon.className = HEADER_ITEM_ICON_CLASS;
            for (let k of Object.keys(labels)) {
                const text = document.createElement('span');
                text.classList.add(HEADER_ITEM_TEXT_CLASS, HEADER_ITEM_TEXT_CLASS + '-' + k);
                text.textContent = labels[k];
                node.appendChild(text);
            }
            node.appendChild(icon);
            return node;
        }
    }
    DirListing.Renderer = Renderer;
    /**
     * The default `IRenderer` instance.
     */
    DirListing.defaultRenderer = new Renderer();
})(DirListing || (DirListing = {}));
/**
 * The namespace for the listing private data.
 */
var Private;
(function (Private) {
    /**
     * Handle editing text on a node.
     *
     * @returns Boolean indicating whether the name changed.
     */
    function userInputForRename(text, edit, original) {
        const parent = text.parentElement;
        parent.replaceChild(edit, text);
        edit.focus();
        const index = edit.value.lastIndexOf('.');
        if (index === -1) {
            edit.setSelectionRange(0, edit.value.length);
        }
        else {
            edit.setSelectionRange(0, index);
        }
        return new Promise(resolve => {
            edit.onblur = () => {
                parent.replaceChild(text, edit);
                resolve(edit.value);
            };
            edit.onkeydown = (event) => {
                switch (event.key) {
                    case 'Enter':
                        event.stopPropagation();
                        event.preventDefault();
                        edit.blur();
                        break;
                    case 'Escape':
                        event.stopPropagation();
                        event.preventDefault();
                        edit.value = original;
                        edit.blur();
                        // Put focus back on the text node. That way the user can, for
                        // example, press the keyboard shortcut to go back into edit mode,
                        // and it will work.
                        text.focus();
                        break;
                    default:
                        break;
                }
            };
        });
    }
    Private.userInputForRename = userInputForRename;
    /**
     * Sort a list of items by sort state as a new array.
     */
    function sort(items, state, sortNotebooksFirst = false, sortFileNamesNaturally = true, translator) {
        const copy = Array.from(items);
        const reverse = state.direction === 'descending' ? 1 : -1;
        /**
         * Compares two items and returns whether they should have a fixed priority.
         * 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.
         */
        function isPriorityOverridden(a, b) {
            if (sortNotebooksFirst) {
                return a.type !== b.type;
            }
            return (a.type === 'directory') !== (b.type === 'directory');
        }
        /**
         * Returns the priority of a file.
         */
        function getPriority(item) {
            if (item.type === 'directory') {
                return 2;
            }
            if (item.type === 'notebook' && sortNotebooksFirst) {
                return 1;
            }
            return 0;
        }
        /**
         * Compare two items by their name using `translator.languageCode`, with fallback to `navigator.language`.
         * When sortFileNamesNaturally is true, uses natural order.
         */
        function compareByName(a, b) {
            var _a;
            // Wokaround for Chromium invalid language code on CI, see
            // https://github.com/jupyterlab/jupyterlab/issues/17079
            const navigatorLanguage = navigator.language.split('@')[0];
            const languageCode = ((_a = translator.languageCode) !== null && _a !== void 0 ? _a : navigatorLanguage).replace('_', '-');
            const localeOptions = {
                numeric: sortFileNamesNaturally,
                sensitivity: 'base'
            };
            try {
                return a.name.localeCompare(b.name, languageCode, localeOptions);
            }
            catch (e) {
                console.warn(`localeCompare failed to compare ${a.name} and ${b.name} under languageCode: ${languageCode}`);
                return a.name.localeCompare(b.name, navigatorLanguage, localeOptions);
            }
        }
        function compare(compare) {
            return (a, b) => {
                // Group directory first, then notebooks, then files
                if (isPriorityOverridden(a, b)) {
                    return getPriority(b) - getPriority(a);
                }
                const compared = compare(a, b);
                if (compared !== 0) {
                    return compared * reverse;
                }
                // Default sorting is alphabetical ascending
                return compareByName(a, b);
            };
        }
        if (state.key === 'last_modified') {
            // Sort by last modified
            copy.sort(compare((a, b) => {
                return (new Date(a.last_modified).getTime() -
                    new Date(b.last_modified).getTime());
            }));
        }
        else if (state.key === 'file_size') {
            // Sort by size
            copy.sort(compare((a, b) => {
                var _a, _b;
                return ((_a = b.size) !== null && _a !== void 0 ? _a : 0) - ((_b = a.size) !== null && _b !== void 0 ? _b : 0);
            }));
        }
        else if (state.key === 'date_created') {
            // Sort by date created
            copy.sort(compare((a, b) => {
                var _a, _b;
                return (new Date((_a = a.created) !== null && _a !== void 0 ? _a : '').getTime() -
                    new Date((_b = b.created) !== null && _b !== void 0 ? _b : '').getTime());
            }));
        }
        else {
            // Sort by name
            copy.sort(compare((a, b) => {
                return compareByName(b, a);
            }));
        }
        return copy;
    }
    Private.sort = sort;
    /**
     * Check if the column is resizable.
     */
    Private.isResizable = (column) => {
        return 'resizable' in column && column.resizable;
    };
    /**
     * Check if the column is sortable.
     */
    Private.isSortable = (column) => {
        return 'sortable' in column && column.sortable;
    };
    /**
     * Get the index of the node at a client position, or `-1`.
     */
    function hitTestNodes(nodes, event) {
        return ArrayExt.findFirstIndex(nodes, node => ElementExt.hitTest(node, event.clientX, event.clientY) ||
            event.target === node);
    }
    Private.hitTestNodes = hitTestNodes;
    /**
     * Update an inline svg caret icon in a node.
     */
    function updateCaret(container, float, state) {
        if (state) {
            (state === 'down' ? caretDownIcon : caretUpIcon).element({
                container,
                tag: 'span',
                stylesheet: 'listingHeaderItem'
            });
            if (float === 'left') {
                container.style.order = '-1';
            }
            else {
                container.style.order = '';
            }
        }
        else {
            LabIcon.remove(container);
            container.className = HEADER_ITEM_ICON_CLASS;
        }
    }
    Private.updateCaret = updateCaret;
    async function createDirectory(manager, path, name) {
        const model = await manager.newUntitled({
            path: path,
            type: 'directory'
        });
        const tmpDirPath = PathExt.join(path, model.name);
        const dirPath = PathExt.join(path, name);
        try {
            await manager.rename(tmpDirPath, dirPath);
        }
        catch (e) {
            // The `dirPath` already exists, remove the temporary new directory
            await manager.deleteFile(tmpDirPath);
        }
        return dirPath;
    }
    Private.createDirectory = createDirectory;
    function isDirectoryEntry(entry) {
        return entry.isDirectory;
    }
    Private.isDirectoryEntry = isDirectoryEntry;
    function isFileEntry(entry) {
        return entry.isFile;
    }
    Private.isFileEntry = isFileEntry;
    function defensiveGetAsEntry(item) {
        if (item.webkitGetAsEntry) {
            return item.webkitGetAsEntry();
        }
        if ('getAsEntry' in item) {
            // See https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
            return item['getAsEntry']();
        }
        return null;
    }
    Private.defensiveGetAsEntry = defensiveGetAsEntry;
    function readEntries(reader) {
        return new Promise((resolve, reject) => reader.readEntries(resolve, reject));
    }
    function readFile(entry) {
        return new Promise((resolve, reject) => entry.file(resolve, reject));
    }
    Private.readFile = readFile;
    async function collectEntries(reader) {
        // Spec requires calling `readEntries` until these are exhausted;
        // in practice this is only required in Chromium-based browsers for >100 files.
        // https://issues.chromium.org/issues/41110876
        const allEntries = [];
        let done = false;
        while (!done) {
            const entries = await readEntries(reader);
            if (entries.length === 0) {
                done = true;
            }
            else {
                allEntries.push(...entries);
            }
        }
        return allEntries;
    }
    Private.collectEntries = collectEntries;
})(Private || (Private = {}));
/**
 * Format bytes to human readable string.
 */
export function formatFileSize(bytes, decimalPoint, k) {
    // https://www.codexworld.com/how-to/convert-file-size-bytes-kb-mb-gb-javascript/
    if (bytes === 0) {
        return '0 B';
    }
    const dm = decimalPoint || 2;
    const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    if (i >= 0 && i < sizes.length) {
        return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
    }
    else {
        return String(bytes);
    }
}
//# sourceMappingURL=listing.js.map