# Source: res/RemoteFileDialog.js

    /**
     * A dialog box letting the user choose a remote open or save file.
     */
    export class RemoteFileDialog extends XUI.Dialog {
        /**
         * Displays a dialog box letting the user choose a remote open or save file.
         *
         * @param {XMLEditor} xmlEditor - the XML editor hosting this dialog box.
         * @param {object} [options={}] options - dialog box options.
         * See description in {@link ResourceStorage#openResource}.
         * @param {boolean} [saveMode=false] saveMode - specify <code>true</code>
         * to display a save file dialog, <code>false</code> to display
         * an open file dialog.
         * @return {Promise} A Promise containing info about the chosen file
         * or <code>null</code> if user clicked <b>Cancel</b>.
         * <dl>
         * <dt><code>uri</code>
         * <dd>Absolute URI of chosen file.
         * <dt><code><i>option_name</i></code>
         * <dd>Only when a dialog box "accessory" has been specified:
         * <code>true</code> if corresponding check box is checked;
         * <code>false</code> otherwise.
         * </dl>
         */
        static showDialog(xmlEditor, options={}, saveMode=false) {
            return new Promise((resolve, reject) => {
                let dialog = new RemoteFileDialog(xmlEditor, options, saveMode,
                                                  resolve);
                dialog.open("center", xmlEditor);
            });
        }

        constructor(xmlEditor, options, saveMode, resolve) {
            super({ title: LocalFileDialog.titleDefaulted(options, saveMode),
                    movable: true, resizable: true, closeable: true,
                    template: RemoteFileDialog.TEMPLATE,
                    buttons: [ { label: "Cancel", action: "cancelAction" },
                               { label: "OK", action: "okAction",
                                 default: true } ] });

            this._xmlEditor = xmlEditor;
            this._resolve = resolve;
            this._saveMode = saveMode;
            this._isLastSelectedURI = false;
            this._initialURI = !options.templateURI? null : options.templateURI;
            if (this._initialURI === null) {
                this._initialURI = window.localStorage.getItem(
                    "XXE.RemoteFileDialog.lastSelectedURI");
                if (this._initialURI !== null) {
                    // In open mode, the basename is not used.
                    // In save mode, reusing the basename of last selected URI
                    // would not make sense.
                    this._initialURI = URIUtil.uriSetBasename(this._initialURI,
                                                              "Untitled.xml");
                    this._isLastSelectedURI = (this._initialURI !== null);
                }
            }
            this._initialPath = null;

            let content = this.contentPane.firstElementChild;
            this._drivePane = content.firstElementChild;
            let chooserPane = content.lastElementChild;

            let folderPane = chooserPane.firstElementChild;
            this._folderSelector = folderPane.firstElementChild /*label*/
                .nextElementSibling;
            this._folderSelector.onchange = this.folderSelected.bind(this);

            this._folderUpButton = this._folderSelector.nextElementSibling;
            this._folderUpButton.firstElementChild.textContent =
                XUI.StockIcon["folder"];
            this._folderUpButton.lastElementChild.textContent =
                XUI.StockIcon["up-bold"];
            this._folderUpButton.onclick = this.folderUpAction.bind(this);

            this._folderNewButton = this._folderUpButton.nextElementSibling;
            this._folderNewButton.firstElementChild.textContent =
                XUI.StockIcon["folder"];
            this._folderNewButton.lastElementChild.textContent =
                XUI.StockIcon["plus"];
            this._folderNewButton.onclick = this.folderNewAction.bind(this);

            let fileScroll = folderPane.nextElementSibling;
            this._fileTable =  fileScroll.firstElementChild;
            this._fileRows = this._fileTable.lastElementChild;
            this._fileRows.onclick = this.fileRowClicked.bind(this);

            let fileForm = fileScroll.nextElementSibling;
            let fileFormChildren = fileForm.children;
            this._fileNameText = fileFormChildren.item(1);
            this._fileNameText.onfocus = this.fileNameTextFocused.bind(this);

            this._selectingFileType = false;
            this._fileTypeSelector = fileFormChildren.item(3);
            RemoteFileDialog.addFileTypes(this._fileTypeSelector,
                                          options.extensions);
            this._fileTypeSelector.onchange = this.fileTypeSelected.bind(this);

            this._optPane = fileForm.nextElementSibling;
            this._optLabel = this._optPane.firstElementChild;
            this._optToggle = this._optLabel.firstElementChild;

            // ---

            if (!options.option ||
                !Array.isArray(options.option) || options.option.length !== 3) {
                this._optionName = null;
                this._optPane.style.display = "none";
            } else {
                this._optionName = options.option[0];
                this._optToggle.checked = options.option[1];
                this._optLabel.appendChild(
                    document.createTextNode(options.option[2]));
            }

            // ---

            this.initDrives();
        }

        // ----------------------------------
        // File types
        // ----------------------------------

        static addFileTypes(select, extensions) {
            select.appendChild(RemoteFileDialog.createOption("*", "All files",
                                                             /*sel*/ true));
            if (!Array.isArray(extensions)) {
                return;
            }

            for (let extension of extensions) {
                if (Array.isArray(extension) && extension.length >= 3) {
                    let list = [];
                    for (let i = 2; i < extension.length; ++i) {
                        let ext = extension[i].trim();
                        if (ext.startsWith(".")) {
                            ext = ext.substring(1);
                        }
                        if (ext.length > 0) {
                            list.push(ext);
                        }
                    }

                    if (list.length > 0) {
                      select.appendChild(
                         RemoteFileDialog.createOption(list.join(';').toLowerCase(),
                                                       /*desc*/ extension[0],
                                                       /*sel*/ false));
                    }
                }
            }
        }

        static createOption(value, text, selected) {
            let option = document.createElement("option");
            option.setAttribute("value", value);
            if (selected) {
                option.setAttribute("selected", "selected");
            }
            option.appendChild(document.createTextNode(text));
            return option;
        }

        selectFileType(extension) {
            this._selectingFileType = true;

            let selectedIndex = -1;
            if (extension === null || extension.length === 0 ||
                extension === "*") {
                selectedIndex = 0; // All files.
            } else {
                extension = extension.toLowerCase();

                for (let option of this._fileTypeSelector.options) {
                    let exts = option.value;
                    if (exts.indexOf(';') >= 0) {
                        exts = exts.split(';');
                    } else {
                        exts = [ exts ];
                    }

                    if (exts.indexOf(extension) >= 0) {
                        selectedIndex = option.index;
                        break;
                    }
                }
            }

            if (selectedIndex < 0) {
                selectedIndex = this._fileTypeSelector.length;
                this._fileTypeSelector.appendChild(
                    RemoteFileDialog.createOption(extension, "*." + extension,
                                                  /*selected*/ false));
            }
            this._fileTypeSelector.options[selectedIndex].selected = true;

            this._selectingFileType = false;
        }

        // ----------------------------------
        // Drives
        // ----------------------------------

        initDrives() {
            this._drives = [];
            this._selectedDrive = null;

            return this._xmlEditor.listRootDirectories()
                .then((drives) => {
                          this.setDrives(drives);
                          return drives;
                      },
                      (error) => {
                          XUI.Alert.showError(
                              `Cannot list root directories:\n${error}`,
                              this._xmlEditor);
                          return null;
                      });
        }

        setDrives(drives) {
            this._drives = drives;
            this._selectedDrive = null;

            XUI.Util.removeAllChildren(this._drivePane);

            let initialURI = this._initialURI;
            this._initialURI = null; // Just used once.
            this._initialPath = null;

            if (drives.length > 0) {
                let selectedDrive = null;
                if (initialURI !== null) {
                    selectedDrive = RemoteFileDialog.findDrive(drives, initialURI);
                    if (selectedDrive === null) {
                        // Drive not found. This detail does not matter anymore.
                        this._isLastSelectedURI = false;
                    } else {
                        // Compute initialPath out of initialURI ---

                        // selectedDrive.uri assumed to end with "/".
                        this._initialPath =
                            "/" + initialURI.substring(selectedDrive.uri.length);

                        this._initialPath =
                            URIUtil.normalizePath(this._initialPath, '/');
                        if (this._initialPath.endsWith("/")) {
                            // In principle, initialURI specifies a file, not a
                            // directory, hence does not end with "/".
                            this._initialPath += "Untitled.xml";
                        }
                    }
                }

                let selectedDriveDiv = null;
                for (let drive of drives) {
                    let driveDiv = this.appendDrive(drive, this._drivePane);
                    if (drive === selectedDrive) {
                        selectedDriveDiv = driveDiv;
                    }
                }

                if (selectedDrive === null) {
                    selectedDrive = drives[0];
                    selectedDriveDiv = this._drivePane.firstElementChild;
                }
                this.selectDrive(selectedDrive, selectedDriveDiv);

                const maxDrives = 6;
                if (drives.length > maxDrives) {
                    // Limit the height of this._drivePane to maxDrives drives.

                    const padding =
                          XUI.Util.getPxProperty(this._drivePane, "padding-top");
                    const driveDiv = this._drivePane.lastElementChild;
                    const margin = XUI.Util.getPxProperty(driveDiv, "margin-top");
                    const maxHeight = (2 * padding) +
                          (maxDrives * driveDiv.offsetHeight) +
                          ((maxDrives - 1) * margin);
                    if (!isNaN(maxHeight)) {
                        this._drivePane.style.maxHeight = String(maxHeight) + "px";
                    }
                }
            }
        }

        static findDrive(drives, uri) {
            uri = uri.toLowerCase();

            let found = null;
            for (let drive of drives) {
                if (uri.startsWith(drive.uri.toLowerCase())) {
                    if (found === null ||
                        found.uri.length < drive.uri.length) { // Better match.
                        found = drive;
                    }
                }
            }

            return found;
        }

        appendDrive(drive, drivePane) {
            let driveDiv = document.createElement("div");
            driveDiv.setAttribute("class", "xui-control xxe-rfd-drive");
            let tooltip = drive.uri;
            if (drive.readonly) {
                tooltip += "\n(read-only)";
            }
            driveDiv.setAttribute("title", tooltip);
            drivePane.appendChild(driveDiv);

            driveDiv.onclick = (e) => {
                XUI.Util.consumeEvent(e);

                if (this._selectedDrive !== drive) {
                    this.selectDrive(drive, driveDiv);
                }
            };

            if (drive.readonly) {
                let badge = document.createElement("div");
                badge.setAttribute("class", "xui-small-icon xxe-rfd-drive-badge");
                badge.textContent = XUI.StockIcon["eye"];
                driveDiv.appendChild(badge);
            }

            let icon = document.createElement("div");
            icon.setAttribute("class", "xui-small-icon xxe-rfd-drive-icon");
            let iconName;
            let iconColor;
            let iconLabel = drive.label.toLowerCase();
            if (iconLabel === "home") {
                iconName = iconLabel;
                iconColor = "#318CE7"; // Bleu de France
            } else if (iconLabel === "computer") {
                iconName = iconLabel;
                iconColor = "#D0417E"; // Magenta (Pantone)
            } else {
                if (!drive.uri.startsWith("file:")) {
                    iconName = "server";
                    iconColor = "#63B76C"; // Fern
                } else {
                    iconName = "drive";
                    iconColor = "#6CB4EE"; // Argentinian Blue
                }
            }
            icon.style.color = iconColor;
            icon.textContent = XUI.StockIcon[iconName];
            driveDiv.appendChild(icon);

            let label = document.createElement("div");
            label.setAttribute("class", "xxe-rfd-drive-label");
            label.textContent = XUI.Util.shortenText(drive.label, 12);
            driveDiv.appendChild(label);

            return driveDiv;
        }

        selectDrive(drive, driveDiv) {
            if (this._selectedDrive !== null) {
                let sel = this._drivePane.querySelector(".xxe-rfd-sel-drive");
                if (sel !== null) {
                    sel.classList.remove("xxe-rfd-sel-drive");
                }

                this._selectedDrive = null;
            }

            if (drive !== null) {
                driveDiv.classList.add("xxe-rfd-sel-drive");

                this._selectedDrive = drive;

                let initialPath = this._initialPath;
                this._initialURI = null;
                this._initialPath = null; // Just used once.

                let folderPath = null;
                let fileName = null;
                if (initialPath !== null) {
                    folderPath = URIUtil.pathParent(initialPath, '/',
                                                    /*trailingSepar*/ false);
                    if (this._saveMode) {
                        fileName = URIUtil.pathBasename(initialPath, '/');
                    }
                }

                if (folderPath === null) {
                    folderPath =
                        !drive.selectedFolder? "/" : drive.selectedFolder.path;
                    // Default folder path. This detail does not matter anymore.
                    this._isLastSelectedURI = false;
                }
                this.selectFolder(drive, folderPath, fileName);
            }
        }

        // ----------------------------------
        // Folders
        // ----------------------------------

        selectFolder(drive, folderPath, fileName=null) {
            this.updateFileTable(drive, folderPath)
                .then((result) => {
                    if (result !== null) {
                        let [actualPath, listing] = result;
                        drive.selectedFolder = { path: actualPath,
                                                 listing: listing };

                        this.updateFolderSelector(actualPath);
                        this.enableFolderButtons(drive, actualPath);

                        if (fileName !== null && actualPath === folderPath) {
                            this.selectRow(fileName);
                        }
                    }
                });
        }

        updateFileTable(drive, folderPath) {
            let folderPathFromLastSelectedURI = this._isLastSelectedURI;
            this._isLastSelectedURI = false; // Just used once.

            const folderInfo = {
                path: folderPath,
                uri: RemoteFileDialog.joinURI(drive, folderPath)
            };

            return this._xmlEditor.listFiles(folderInfo.uri)
                .then((listing) => {
                    if (Array.isArray(listing)) {
                        return listing;
                    } else {
                        // Not a folder.
                        if (folderPath === "/") {
                            // Nothing to do. Error reported during next step.
                            return null;
                        } else {
                            if (!folderPathFromLastSelectedURI) {
                                this.reportListFolderError(folderInfo.path,
                                                           "not a folder");
                            }
                            // Otherwise, folder path comes from
                            // window.localStorage. Do not bother user with this
                            // error.

                            // Fallback to "/".
                            folderInfo.path = "/";
                            folderInfo.uri = RemoteFileDialog.joinURI(drive, "/");
                            return this._xmlEditor.listFiles(folderInfo.uri);
                        }
                    }
                })
                .then((listing) => {
                    if (!Array.isArray(listing)) {
                        this.reportListFolderError(folderInfo.path, "not a folder");
                        // Give up.
                        return null;
                    }

                    this.updateFileRows(listing, this.getAcceptedExtensions());
                    return [folderInfo.path, listing];
                })
                .catch((error) => {
                    this.reportListFolderError(folderInfo.path, error);
                    return null;
                });
        }

        static joinURI(drive, filePath, fileName=null) {
            if (fileName !== null) {
                filePath = RemoteFileDialog.joinPath(filePath, fileName);
            }

            let uri = drive.uri;
            if (!uri.endsWith("/")) {
                uri += "/";
            }

            const parts = RemoteFileDialog.splitPath(filePath);
            for (let i = 0; i < parts.length; ++i) {
                if (i > 0) {
                    uri += "/";
                }
                uri += encodeURIComponent(parts[i]);
            }

            return uri;
        }

        static joinPath(folderPath, fileName) {
            let path = folderPath;
            if (!path.endsWith("/")) {
                path += "/";
            }
            path += fileName;

            return path;
        }

        static splitPath(path) {
            // Note that all paths are absolute here.

            if (path.startsWith("/")) {
                path = path.substring(1);
            }
            if (path.endsWith("/")) {
                path = path.substring(0, path.length - 1);
            }

            if (path.length > 0) {
                return path.split("/");
            } else {
                return [];
            }
        }

        reportListFolderError(folderPath, error) {
            XUI.Alert.showError(`Cannot list folder\n"${folderPath}":\n${error}`,
                                this._xmlEditor);
        }

        // ----------------------------------
        // Folder selector
        // ----------------------------------

        updateFolderSelector(folderPath) {
            XUI.Util.removeAllChildren(this._folderSelector);

            const parts = RemoteFileDialog.splitPath(folderPath);
            const lastPart = parts.length-1;

            folderPath = "/";
            let prevOption =
                RemoteFileDialog.createOption(folderPath, folderPath,
                                              /*selected*/ (-1 === lastPart));
            this._folderSelector.appendChild(prevOption);

            for (let i = 0; i <= lastPart; ++i) {
                if (i > 0) {
                    folderPath += "/";
                }
                folderPath += parts[i];

                let option =
                    RemoteFileDialog.createOption(folderPath, folderPath,
                                                  /*selected*/ (i === lastPart));
                this._folderSelector.insertBefore(option, prevOption);
                prevOption = option;
            }
        }

        folderSelected(event) {
            this.selectFolder(this._selectedDrive, event.target.value);
        }

        // ----------------------------------
        // Folder buttons
        // ----------------------------------

        enableFolderButtons(drive, folderPath) {
            RemoteFileDialog.enableButton(this._folderUpButton, folderPath !== "/");
            RemoteFileDialog.enableButton(this._folderNewButton, !drive.readonly);
        }

        static enableButton(button, enable) {
            if (enable) {
                button.removeAttribute("disabled");
                button.classList.remove("xui-control-disabled");
            } else {
                button.setAttribute("disabled", "disabled");
                button.classList.add("xui-control-disabled");
            }
        }

        folderUpAction(event) {
            let selectedFolder = this.getSelectedFolder();
            if (selectedFolder === null) {
                // Should not happen (unless we have no drive).
                return;
            }

            let folderPath = selectedFolder.path;
            if (folderPath === "/") {
                // Nothing to do.
                return;
            }
            folderPath = URIUtil.pathParent(folderPath, '/',
                                            /*trailingSepar*/ false);

            this.selectFolder(this._selectedDrive, folderPath);
        }

        getSelectedFolder() {
            const selectedDrive = this._selectedDrive;
            if (!selectedDrive) {
                return null;
            }

            const selectedFolder = selectedDrive.selectedFolder;
            if (!selectedFolder ||
                !selectedFolder.path ||
                !Array.isArray(selectedFolder.listing)) {
                return null;
            }

            // This also ensures that there is a selected drive.
            return selectedFolder;
        }

        folderNewAction(event) {
            let selectedFolder = this.getSelectedFolder();
            if (selectedFolder === null) {
                // Should not happen (unless we have no drive).
                return;
            }

            let folderName = [];

            XUI.Prompt.showPrompt("Create New Folder", "Folder name:",
                                  /*cols*/ 30, /*initValue*/ null,
                                  /*values*/ null, /*checker*/ null,
                                  this._xmlEditor)
                .then((answer) => {
                    if (answer === null) {
                        // Canceled by user.
                        return false;
                    } else {
                        let error = null;
                        if (answer.indexOf("/") >= 0) {
                            error = `"${answer}" is not a file basename.`;
                        } else if (RemoteFileDialog.getFileInfo(selectedFolder,
                                                                answer) !== null) {
                            error = `File "${answer}" already exists.`;
                        }
                        if (error !== null) {
                            XUI.Alert.showError(error, this._xmlEditor);
                            // Will not attempt to create it.
                            return false;
                        }

                        folderName.push(answer);
                        return this._xmlEditor.createDirectory(
                            RemoteFileDialog.joinURI(this._selectedDrive,
                                                     selectedFolder.path,
                                                     folderName[0]));
                    }
                })
                .then((created) => {
                    if (created) {
                        this.selectFolder(this._selectedDrive, selectedFolder.path,
                                          folderName[0]);
                    }
                    // Otherwise canceled by user or file exists.
                })
                .catch((error) => {
                    XUI.Alert.showError(
                        `Cannot create folder "${folderName[0]}":\n${error}`,
                        this._xmlEditor);
                });
        }

        static getFileInfo(selectedFolder, fileName) {
            let fileInfo = selectedFolder.listing.find((entry) => {
                return (entry.name === fileName);
            });

            return !fileInfo? null : fileInfo;
        }

        // ----------------------------------
        // File rows
        // ----------------------------------

        updateFileRows(listing, acceptedExts) {
            XUI.Util.removeAllChildren(this._fileRows);

            for (let file of listing) {
                if (!file.directory &&
                    !RemoteFileDialog.acceptFileName(file.name, acceptedExts)) {
                    continue;
                }

                let row = document.createElement("tr");
                // Custom property set on a tbody/tr.
                row.xxeFileInfo = file;

                let cell = document.createElement("td");

                let fileIcon = document.createElement("span");
                let fileIconClass, fileIconName;
                if (file.directory) {
                    fileIconClass = "xxe-rfd-dir-icon";
                    fileIconName = "folder";
                } else {
                    fileIconClass = "xxe-rfd-file-icon";
                    fileIconName = "file";
                }
                fileIcon.setAttribute("class", "xui-small-icon " + fileIconClass);
                fileIcon.textContent = XUI.StockIcon[fileIconName];
                cell.appendChild(fileIcon);

                cell.appendChild(document.createTextNode(file.name));
                row.appendChild(cell);

                cell = document.createElement("td");
                cell.textContent = URIUtil.formatFileSize(file.size, "\u00A0");
                row.appendChild(cell);

                cell = document.createElement("td");
                cell.textContent = URIUtil.formatFileDate(file.date, "\u00A0");
                row.appendChild(cell);

                this._fileRows.appendChild(row);
            }

            this._fileTable.scrollIntoView({ block: "start", inline: "start" });
        }

        static acceptFileName(fileName, acceptedExts) {
            if (acceptedExts === null || acceptedExts.length === 0) {
                // No filter.
                return true;
            }

            let ext = URIUtil.pathExtension(fileName, '/');
            if (ext === null || (ext = ext.trim()).length === 0) {
                return false;
            }
            ext = ext.toLowerCase();

            return (acceptedExts.indexOf(ext) >= 0);
        }

        fileRowClicked(event) {
            if (event.button === 0) { // primary button
                XUI.Util.consumeEvent(event);

                switch (event.detail) {
                case 1: // First click.
                    this.fileRowSelected(event);
                    break;
                case 2: // Second click.
                    this.okAction();
                    break;
                }
            }
        }

        fileRowSelected(event) {
            let elem = event.target;
            while (elem !== null) {
                if ("xxeFileInfo" in elem) {
                    // Custom property set on a tbody/tr.
                    this.selectRow(elem.xxeFileInfo.name);
                    break;
                }

                if (elem.localName === "tbody") {
                    // No more rows.
                    break;
                }
                elem = elem.parentElement;
            }
        }

        selectRow(fileName) {
            this.unselectRow();

            let row = null;
            for (let r of this._fileRows.children) {
                // Custom property set on a tbody/tr.
                if (r.xxeFileInfo && r.xxeFileInfo.name === fileName) {
                    row = r;
                    break;
                }
            }

            if (row !== null) {
                row.classList.add("xxe-rfd-sel-file");
                row.scrollIntoViewIfNeeded(/*center*/ true);

                if (!row.xxeFileInfo.directory) {
                    this._fileNameText.value = fileName;
                }
                // Otherwise, leave typed file name as is.
            }
        }

        unselectRow() {
            let sel = this._fileRows.querySelector(".xxe-rfd-sel-file");
            if (sel !== null) {
                sel.classList.remove("xxe-rfd-sel-file");
            }
        }

        // ----------------------------------
        // File name and type controls
        // ----------------------------------

        fileNameTextFocused(event) {
            // The user may type something which no longer matches selected row.
            this.unselectRow();
        }

        fileTypeSelected(event) {
            if (this._selectingFileType) {
                return;
            }

            let selectedFolder = this.getSelectedFolder();
            if (selectedFolder === null) {
                // Should not happen (unless we have no drive).
                return null;
            }

            this.updateFileRows(selectedFolder.listing,
                                this.getAcceptedExtensions());
        }

        getAcceptedExtensions() {
            let sel = this._fileTypeSelector.selectedIndex;
            if (sel < 0) {
                // Should not happen.
                return null;
            }

            let extList = this._fileTypeSelector.options[sel].value;
            if (extList === "*") {
                // No filter.
                return null;
            }

            return extList.split(';');
        }

        // ----------------------------------
        // Dialog buttons
        // ----------------------------------

        dialogClosed(result) {
            // Close icon clicked ==> null result, which is just fine.
            this._resolve(result);
        }

        cancelAction() {
            this.close(null);
        }

        getFileChoice() {
            let selectedFolder = this.getSelectedFolder();
            if (selectedFolder === null) {
                // Should not happen (unless we have no drive).
                return null;
            }

            // If a folder is selected, visit it no matter what the user has typed
            // as a file name. ---

            let selectedFileInfo = null;
            let sel = this._fileRows.querySelector(".xxe-rfd-sel-file");
            if (sel !== null && sel.xxeFileInfo && sel.xxeFileInfo.name) {
                selectedFileInfo = sel.xxeFileInfo;
            }

            if (selectedFileInfo !== null && selectedFileInfo.directory) {
                const folderPath = RemoteFileDialog.joinPath(selectedFolder.path,
                                                             selectedFileInfo.name);
                this.selectFolder(this._selectedDrive, folderPath);
                return null;
            }

            // No selected row or selected row specifies a file, priority to what
            // the user has typed as a file name (if any). ---

            let fileInfo = null;
            let fileName = this._fileNameText.value.trim();
            if (fileName.length === 0 && selectedFileInfo !== null) {
                // User has not typed a file name and a row specifying a file is
                // selected. Use it.
                fileName = selectedFileInfo.name;
                fileInfo = selectedFileInfo;
            }

            if (fileName.length === 0 ||
                fileName.indexOf('/') >= 0) { // Not supported.
                // No usable file name. Give up.
                XUI.Util.badTextField(this._fileNameText);
                return null;
            }

            if (fileInfo === null) {
                // No selected row specifying a file. User has just typed a file
                // name.
                fileInfo = RemoteFileDialog.getFileInfo(selectedFolder, fileName);
            }

            let choice = {};

            if (fileInfo === null) {
                // Specified file does not exist. ---

                // May be user typed a wildcard?
                if (fileName.indexOf('*') >= 0) {
                    let ext = null;

                    if (fileName === "*") {
                        fileName = "*.*";
                    }
                    if (fileName.startsWith("*.")) {
                        ext = URIUtil.pathExtension(fileName, '/');
                        if (ext === null || ext.length === 0) {
                            ext = null;
                        }
                    }

                    if (ext === null) {
                        // Not supported.
                        XUI.Util.badTextField(this._fileNameText);
                    } else {
                        this._fileNameText.value = ""; // Wildcard no longer useful.
                        this.selectFileType(ext);
                        this.fileTypeSelected(/*event*/ null);
                    }

                    return null;
                }

                if (!this._saveMode) {
                    // Cannot open a file which does not exist.
                    XUI.Util.badTextField(this._fileNameText);
                    return null;
                }
            } else {
                // Specified file exists. ---

                if (fileInfo.directory) {
                    this._fileNameText.value = ""; // Dir name no longer useful.
                    const folderPath =
                          RemoteFileDialog.joinPath(selectedFolder.path, fileName);
                    this.selectFolder(this._selectedDrive, folderPath);
                    return null;
                }

                if (this._saveMode) {
                    choice.confirmCanOverwrite = true;
                }
            }

            choice.uri = RemoteFileDialog.joinURI(this._selectedDrive,
                                                  selectedFolder.path, fileName);

            // ---

            if (this._optionName !== null) {
                choice[this._optionName] = this._optToggle.checked;
            }

            return choice;
        }

        okAction() {
            let choice = this.getFileChoice();
            if (choice === null) {
                return;
            }
            window.localStorage.setItem("XXE.RemoteFileDialog.lastSelectedURI",
                                        choice.uri);

            if (choice.confirmCanOverwrite) {
                delete choice.confirmCanOverwrite;

                return XUI.Confirm.showConfirm(
                    `File\n${choice.uri}\nalready exists. Overwrite?`,
                    this._xmlEditor)
                    .then((confirmed) => {
                        if (confirmed) {
                            this.close(choice);
                        }
                    });
            } else {
                this.close(choice);
            }
        }
    }

    RemoteFileDialog.TEMPLATE = document.createElement("template");
    RemoteFileDialog.TEMPLATE.innerHTML = `
    <div class="xxe-rfd-pane">
      <div class="xui-control xxe-rfd-drive-pane">
      </div>
      <div class="xxe-rfd-chooser-pane">
        <div class="xxe-rfd-folder-pane">
          <span class="xxe-rfd-folder-label">Folder:</span>
          <select autocomplete="off"
            class="xui-control xxe-rfd-folder-select"></select>
          <button disabled="disabled" title="Up one level"
                  class="xui-control xui-control-disabled
                         xui-small-icon xxe-rfd-folder-up">
            <span class="xxe-rfd-folder-icon"></span><span
              class="xxe-rfd-folder-badge"></span>
          </button>
          <button disabled="disabled" title="Create new folder"
                  class="xui-control xui-control-disabled
                         xui-small-icon xxe-rfd-folder-new">
            <span class="xxe-rfd-folder-icon"></span><span
              class="xxe-rfd-folder-badge"></span>
          </button>
        </div>
        <div class="xxe-rfd-file-scroll">
          <table class="xxe-rfd-file-table">
            <colgroup class="xxe-rfd-ncol"></colgroup>
            <colgroup class="xxe-rfd-scol"></colgroup>
            <colgroup class="xxe-rfd-dcol"></colgroup>
            <thead><th>Name</th><th>Size</th><th>Last Modified</th></thead>
            <tbody></tbody>
          </table>
        </div>
        <div class="xxe-rfd-file-form">
          <span class="xxe-rfd-file-nlabel">File name:</span>
          <input type="text" class="xui-control xxe-rfd-file-name"
                 spellcheck="false" autocomplete="off" />
          <span class="xxe-rfd-file-tlabel">File type:</span>
          <select autocomplete="off"
            class="xui-control xxe-rfd-file-type"></select>
        </div>
        <div class="xxe-rfd-opt-pane">
          <label class="xxe-rfd-opt-label">
            <input type="checkbox" class="xxe-rfd-opt-toggle"/>
          </label>
        </div>
      </div>
    </div>
    `;