Source: xxe/res/LocalFileDialog.js

/**
 * A dialog box letting the user open a local file or save data to a local file.
 */
export class LocalFileDialog extends XUI.Dialog {
    /**
     * Displays a dialog box letting the user open a local file or 
     * save specified data to a local 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 {Blob} [savedData=null] savedData - the data to be saved to 
     * a file; <code>null</code> to display an open file dialog.
     * @return {Promise} A Promise containing info about the open or save file
     * or <code>null</code> if user clicked <b>Cancel</b>.
     * <dl>
     * <dt><code>file</code>
     * <dd>Opened file.
     * <dt><code>fileHandle</code>
     * <dd>Saved file handle; always <code>null</code> 
     * when <code>!FSAccess.isAvailable()</code>.
     * <dt><code>uri</code>
     * <dd>Absolute "file:" URI of selected 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>
     * <p>On some Blink-based browsers, for example 
     * <a href="https://brave.com/">Brave</a>,  
     * the <a href="https://fs.spec.whatwg.org/">"File System Access API"</a> 
     * is disabled by default. In order to enable it, visit 
     * <code>brave://flags/#file-system-access-api</code> and change 
     * this setting from "Default" to "Enabled".
     */
    static showDialog(xmlEditor, options={}, savedData=null) {
        return new Promise((resolve, reject) => { 
            let dialog = new LocalFileDialog(xmlEditor, options, savedData,
                                             resolve);
            dialog.open("center", xmlEditor);
        });
    }
    
    constructor(xmlEditor, options, savedData, resolve) {
        super({ title: LocalFileDialog.titleDefaulted(options, savedData),
                movable: true, resizable: true, closeable: true,
                template: LocalFileDialog.TEMPLATE,
                buttons: [ { label: "Cancel", action: "cancelAction" },
                           { label: "OK", action: "okAction",
                             default: true } ] });

        this._xmlEditor = xmlEditor;
        this._options = options;
        this._savedData = savedData;
        this._resolve = resolve;
        
        let pane = this.contentPane.firstElementChild; 
        this._step1 = pane.querySelector(".xxe-lfd-step1");
        this._browseLabel = pane.querySelector(".xxe-lfd-browse-label");
        this._browseButton = pane.querySelector(".xxe-lfd-browse");
        this._browseButton.onclick = this.chooseFileAction.bind(this);

        this._step2 = pane.querySelector(".xxe-lfd-step2");
        this._dirPathText = pane.querySelector(".xxe-lfd-dir-path");
        
        let pathSepar = pane.querySelector(".xxe-lfd-path-separ");
        pathSepar.textContent = FILE_PATH_SEPARATOR;
        
        this._fileNameText = pane.querySelector(".xxe-lfd-file-name");
        const openMode = !this._savedData;
        if (FSAccess.isAvailable() || openMode) {
            this._fileNameText.setAttribute("readonly", "readonly");
        }

        this._optPane = pane.querySelector(".xxe-lfd-opt-pane");
        this._optLabel = pane.querySelector(".xxe-lfd-opt-label");
        this._optToggle = pane.querySelector(".xxe-lfd-opt-toggle");

        // ---

        if (openMode) {
            this._browseButton.textContent = XUI.StockIcon["folder-open"];
        } else {
            this._browseLabel.textContent = "Save file:";
            this._browseButton.textContent = XUI.StockIcon["floppy"];
        }

        // ---

        let suggestedDirPath = null;
        let suggestedFileName = null;
        
        let templatePath = !options.templateURI? null :
            URIUtil.uriToFilePath(options.templateURI);
        if (templatePath !== null) {
            suggestedDirPath =
                URIUtil.pathParent(templatePath, FILE_PATH_SEPARATOR,
                                   /*trailingSepar*/ false);
            suggestedFileName =
                URIUtil.pathBasename(templatePath, FILE_PATH_SEPARATOR);
        }

        this._suggestedFileName = null;
        if (!openMode && suggestedFileName) {
            // File chooser option: suggest a save file name.
            this._suggestedFileName = suggestedFileName;
        }
        
        this.rememberDirPath(suggestedDirPath, /*addDataList*/ true);
        if (!FSAccess.isAvailable()) {
            // In open mode, this._fileNameText is read-only and the
            // corresponding datalist is not useful.
            this.rememberFileName(suggestedFileName, /*addDataList*/ !openMode);
        }
        // Otherwise, this._fileNameText is always read-only and the
        // corresponding datalist is never useful.
        
        // ---

        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._openedFile = null;
        this._savedFileHandle = null;
    }

    static titleDefaulted(options, savedData) {
        if (!options.title) {
            if (!savedData) {
                return "Open";
            } else {
                return "Save";
            }
        } else {
            return options.title;
        }
    }

    rememberDirPath(dirPath, addDataList) {
        let valueList =
            XUI.Util.rememberDatalistItem("XXE.LocalFileDialog.lastDirPaths",
                                          dirPath);
        
        if (addDataList) {
            XUI.Util.attachDatalist(this._dirPathText, valueList,
                                    this.contentPane.firstElementChild);
        }
    }

    rememberFileName(fileName, addDataList) {
        let valueList =
            XUI.Util.rememberDatalistItem("XXE.LocalFileDialog.lastFileNames",
                                          fileName);
        
        if (addDataList) {
            XUI.Util.attachDatalist(this._fileNameText, valueList,
                                    this.contentPane.firstElementChild);
        }
    }

    chooseFileAction(event) {
        let opts = { id: "XXE_localFileDialog", multiple: false,
                     startIn: "documents", excludeAcceptAllOption: false };
        
        const openMode = !this._savedData;
        if (openMode) {
            LocalFileDialog.addExtensions(this._options.extensions, opts);
        } else {
            if (this._suggestedFileName !== null) {
                opts.fileName = this._suggestedFileName;
            }
        }

        if (openMode) {
            FSAccess.fileOpen(opts)
                .then((file) => {
                          if (file) {
                              this._openedFile = file;
                              this._fileNameText.value = file.name;
                              this.proceedToStep2();
                          }
                      },
                      (error) => {
                          this.chooseFileError(error);
                      });
        } else {
            // Always interactive. Displays a dialog box.
            FSAccess.fileSave(this._savedData, opts, /*fileHandle*/ null,
                              /*throwIfUnusableHandle*/ false)
                .then((handle) => {
                          if (FSAccess.isAvailable()) {
                              if (handle) {
                                  this._savedFileHandle = handle;
                                  this._fileNameText.value = handle.name;
                                  this.proceedToStep2();
                              }
                          } else {
                              // Otherwise, no way to get the chosen file name
                              // or even to tell whether user clicked OK or
                              // Cancel.  So we rely on the user typing the
                              // chosen file name.
                              this.proceedToStep2();
                          }
                      },
                      (error) => {
                          this.chooseFileError(error);
                      });
        }
    }

    static addExtensions(exts, opts) {
        if (FSAccess.isAvailable()) {
            // Seems unusable with showXXXFilePicker.
            return;
        }
        // Otherwise, anything more complicated than just opts.extensions does
        // not seem to work.
        
        if (Array.isArray(exts) && exts.length > 0) {
            opts.extensions = [];
            
            for (let ext of exts) {
                if (Array.isArray(ext) && ext.length >= 3) {
                    // Example: ["DocBook files", "application/docbook+xml",
                    //           "dbk", "docb"].
                    for (let i = 2; i < ext.length; ++i) {
                        let suffix = ext[i].trim().toLowerCase();
                        if (suffix.length > 0) {
                            if (!suffix.startsWith(".")) {
                                suffix = "." + suffix;
                            }
                            opts.extensions.push(suffix);
                        }
                    }
                }
            }
        }
    }
        
    chooseFileError(error) {
        if (error.name !== "AbortError") {
            // For example: NotAllowedError when the user refuses to grant
            // permission to save the file.
            
            this._openedFile = null;
            this._savedFileHandle = null;
            
            XUI.Alert.showError(
                `Cannot display the file chooser dialog:\n${error}`,
                this._xmlEditor);
        }
        // AbortError means: cancelled by user.
    }

    proceedToStep2() {
        this._step1.classList.remove("xxe-lfd-curstep");
        this._step2.classList.add("xxe-lfd-curstep");
        this._dirPathText.removeAttribute("disabled");
        this._fileNameText.removeAttribute("disabled");
    }
    
    dialogClosed(result) {
        // Close icon clicked ==> null result, which is just fine.
        this._resolve(result);
    }
    
    cancelAction() {
        this.close(null);
    }
    
    okAction() {
        let choice = {};
        
        const openMode = !this._savedData;
        if (openMode) {
            if (this._openedFile === null) {
                this._browseButton.focus();
                return;
            }
            
            choice.file = this._openedFile;
        } else {
            if (FSAccess.isAvailable()) {
                if (this._savedFileHandle === null) {
                    this._browseButton.focus();
                    return;
                }
                
                choice.fileHandle = this._savedFileHandle;
            } else {
                // No fileHandle. So we rely on the user typing the chosen
                // file name.
                choice.fileHandle = null;
            }
        }

        // ---
        
        let dirPath = this._dirPathText.value.trim();
        if (dirPath.length === 0) {
            XUI.Util.badTextField(this._dirPathText);
            return;
        }
        
        let fileName = this._fileNameText.value.trim();
        if (fileName.length === 0 ||
            fileName.indexOf(FILE_PATH_SEPARATOR) >= 0) {
            XUI.Util.badTextField(this._fileNameText);
            return;
        }

        let filePath = dirPath;
        if (!filePath.endsWith(FILE_PATH_SEPARATOR)) {
            filePath += FILE_PATH_SEPARATOR;
        }
        filePath += fileName;
        
        if (!URIUtil.isAbsolutePath(filePath, FILE_PATH_SEPARATOR)) {
            XUI.Util.badTextField(this._dirPathText);
            return;
        }

        // "Normalize" dirPath.
        dirPath = URIUtil.pathParent(filePath, FILE_PATH_SEPARATOR,
                                     /*trailingSepar*/ false);
        this.rememberDirPath(dirPath, /*addDataList*/ false);
        
        if (!FSAccess.isAvailable()) {
            // Remember everything the user has selected or typed.
            this.rememberFileName(fileName, false);
        }
        // Otherwise, this._fileNameText is always read-only and the
        // corresponding datalist is never useful.
        
        choice.uri = URIUtil.pathToFileURI(filePath);
        
        // ---
        
        if (this._optionName !== null) {
            choice[this._optionName] = this._optToggle.checked;
        }

        this.close(choice);
    }
}

LocalFileDialog.TEMPLATE = document.createElement("template");
LocalFileDialog.TEMPLATE.innerHTML = `
<div class="xxe-lfd-pane">
  <div class="xxe-lfd-browse-pane"><span class="xxe-lfd-step1 
    xxe-lfd-curstep">1.</span>
    <span class="xxe-lfd-browse-label">Open file:</span>
    <button type="button" title="Choose file..."
            class="xui-control xui-small-icon xxe-lfd-browse"></button>
  </div>
  <div class="xxe-lfd-path-label"><span class="xxe-lfd-step2">2.</span>
  Specify the <strong>absolute</strong> path of chosen file:</div>
  <div class="xxe-lfd-path-pane">
    <input type="text" size="50" class="xui-control xxe-lfd-dir-path" 
           spellcheck="false" autocomplete="off" disabled="disabled" />
    <span class="xxe-lfd-path-separ"></span>
    <input type="text" size="20" class="xui-control xxe-lfd-file-name" 
           spellcheck="false" autocomplete="off" disabled="disabled" />
  </div>
  <div class="xxe-lfd-path-hint"><strong>Required.</strong>
  For security reasons, this application has no way<br />
  to automatically determine the absolute path of chosen file.</div>
  <div class="xxe-lfd-opt-pane">
    <label class="xxe-lfd-opt-label">
      <input type="checkbox" class="xxe-lfd-opt-toggle"/>
    </label>
  </div>
</div>
`;