Source: xxe/res/RemoteFileDialog.js

/**
 * A dialog box letting the user choose a remote open or save file.
 */
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>
`;