/**
* 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>
`;