2024-01-16 16:25:39 +01:00
|
|
|
|
/**
|
|
|
|
|
* EGroupware eTemplate2 - File selection WebComponent
|
|
|
|
|
*
|
|
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
|
|
|
* @package api
|
|
|
|
|
* @link https://www.egroupware.org
|
|
|
|
|
* @author Nathan Gray
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
2024-05-24 23:22:51 +02:00
|
|
|
|
import {html, LitElement, nothing, PropertyValues, render, TemplateResult} from "lit";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
import shoelace from "../Styles/shoelace";
|
|
|
|
|
import styles from "./Et2VfsSelect.styles";
|
|
|
|
|
import {property} from "lit/decorators/property.js";
|
|
|
|
|
import {state} from "lit/decorators/state.js";
|
|
|
|
|
import {ifDefined} from "lit/directives/if-defined.js";
|
2024-02-23 00:21:28 +01:00
|
|
|
|
import {classMap} from "lit/directives/class-map.js";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
import {repeat} from "lit/directives/repeat.js";
|
|
|
|
|
import {SelectOption} from "../Et2Select/FindSelectOptions";
|
|
|
|
|
import {DialogButton, Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
|
|
|
|
import {HasSlotController} from "../Et2Widget/slot";
|
2024-02-12 18:32:28 +01:00
|
|
|
|
import {egw, IegwAppLocal} from "../../jsapi/egw_global";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
import {Et2Select} from "../Et2Select/Et2Select";
|
2024-01-18 00:32:09 +01:00
|
|
|
|
import {Et2VfsSelectRow} from "./Et2VfsSelectRow";
|
2024-01-29 17:57:52 +01:00
|
|
|
|
import {Et2VfsPath} from "./Et2VfsPath";
|
2024-02-23 00:21:28 +01:00
|
|
|
|
import {SearchMixin, SearchResult, SearchResultElement, SearchResultsInterface} from "../Et2Widget/SearchMixin";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
/**
|
2024-02-21 00:52:47 +01:00
|
|
|
|
* @summary Select files (including directories) from the VFS.
|
|
|
|
|
*
|
2024-02-21 17:11:45 +01:00
|
|
|
|
* The dialog does not do anything with the files, just handles the UI to select them.
|
2024-01-16 16:25:39 +01:00
|
|
|
|
*
|
2024-02-20 17:19:51 +01:00
|
|
|
|
* @dependency et2-box
|
|
|
|
|
* @dependency et2-button
|
2024-01-16 16:25:39 +01:00
|
|
|
|
* @dependency et2-dialog
|
2024-02-20 17:19:51 +01:00
|
|
|
|
* @dependency et2-image
|
|
|
|
|
* @dependency et2-searchbox
|
2024-01-16 16:25:39 +01:00
|
|
|
|
* @dependency et2-select
|
2024-01-18 00:32:09 +01:00
|
|
|
|
* @dependency et2-vfs-select-row
|
2024-01-16 16:25:39 +01:00
|
|
|
|
*
|
2024-01-29 17:57:52 +01:00
|
|
|
|
* @slot title - Optional additions to title.
|
2024-01-16 16:25:39 +01:00
|
|
|
|
* @slot toolbar - Toolbar containing controls for search & navigation
|
|
|
|
|
* @slot prefix - Before the toolbar
|
|
|
|
|
* @slot suffix - Like prefix, but after
|
|
|
|
|
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
|
|
|
|
* @slot footer - Customise the dialog footer
|
|
|
|
|
*
|
|
|
|
|
* @event change - Emitted when the control's value changes.
|
|
|
|
|
*
|
2024-02-09 16:47:04 +01:00
|
|
|
|
* @csspart toolbar - controls at the top
|
2024-02-21 00:20:00 +01:00
|
|
|
|
* @csspart path - Et2VfsPath control
|
2024-02-09 16:47:04 +01:00
|
|
|
|
* @csspart listbox - The list of files
|
|
|
|
|
* @csspart mimefilter - Mime filter select
|
2024-01-16 16:25:39 +01:00
|
|
|
|
* @csspart form-control-help-text - The help text's wrapper.
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
type Constructor<T = {}> = new (...args : any[]) => T;
|
|
|
|
|
|
|
|
|
|
export class Et2VfsSelectDialog
|
|
|
|
|
extends SearchMixin<Constructor<any> & typeof LitElement, FileInfo, FileResultsInterface>(Et2InputWidget(LitElement))
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
|
|
|
|
static get styles()
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
shoelace,
|
|
|
|
|
...super.styles,
|
|
|
|
|
styles
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Currently selected files */
|
|
|
|
|
@property() value : string[] = [];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The dialog’s label as displayed in the header.
|
|
|
|
|
* You should always include a relevant label, as it is required for proper accessibility.
|
|
|
|
|
*/
|
|
|
|
|
@property() title : string = "Select";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dialog mode
|
|
|
|
|
* Quickly sets button label, multiple, selection and for "select-dir", mime-type
|
|
|
|
|
**/
|
2024-01-18 00:32:09 +01:00
|
|
|
|
@property({type: String}) mode : "open" | "open-multiple" | "saveas" | "select-dir";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
/** Button label */
|
2024-01-18 00:32:09 +01:00
|
|
|
|
@property({type: String}) buttonLabel : string = "Select";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
/** Provide a suggested filename for saving */
|
|
|
|
|
@property() filename : string = "";
|
|
|
|
|
|
|
|
|
|
/** Allow selecting multiple files */
|
|
|
|
|
@property({type: Boolean}) multiple = false;
|
|
|
|
|
|
|
|
|
|
/** Start path in VFS. Leave unset to use the last used path. */
|
|
|
|
|
@property() path : string = "";
|
|
|
|
|
|
|
|
|
|
/** Limit display to the given mime-type */
|
|
|
|
|
@property() mime : string | string[] | RegExp = "";
|
|
|
|
|
|
|
|
|
|
/** List of mimetypes to allow user to filter. */
|
2024-03-06 22:25:48 +01:00
|
|
|
|
@property({type: Array}) mimeList : SelectOption[] = [
|
2024-01-22 23:19:21 +01:00
|
|
|
|
{
|
|
|
|
|
value: "/(application\\/vnd.oasis.opendocument.text|application\\/vnd.openxmlformats-officedocument.wordprocessingml.document)/i",
|
|
|
|
|
label: "Documents"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: "/(application\\/vnd.oasis.opendocument.spreadsheet|application\\/vnd.openxmlformats-officedocument.spreadsheetml.sheet)/i",
|
|
|
|
|
label: "Spreadsheets"
|
|
|
|
|
},
|
|
|
|
|
{value: "image/", label: "Images"},
|
2024-05-24 23:22:51 +02:00
|
|
|
|
{value: "video/", label: "Videos"},
|
|
|
|
|
{value: "message/rfc822", label: "Email"}
|
2024-01-22 23:19:21 +01:00
|
|
|
|
];
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
/** The select's help text. If you need to display HTML, use the `help-text` slot instead. */
|
|
|
|
|
@property({attribute: 'help-text'}) helpText = '';
|
|
|
|
|
|
|
|
|
|
@state() open : boolean = false;
|
2024-02-23 00:21:28 +01:00
|
|
|
|
@state() currentResult : Et2VfsSelectRow;
|
2024-01-18 00:32:09 +01:00
|
|
|
|
@state() selectedFiles : Et2VfsSelectRow[] = [];
|
2024-02-09 17:15:11 +01:00
|
|
|
|
@state() _pathWritable : boolean = false;
|
2024-01-18 00:32:09 +01:00
|
|
|
|
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
// SearchMixinInterface //
|
|
|
|
|
@property() searchUrl : string = "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelectFiles";
|
|
|
|
|
|
|
|
|
|
// End SearchMixinInterface //
|
|
|
|
|
|
|
|
|
|
// Still need some server-side info
|
|
|
|
|
protected _serverContent : Promise<any> = Promise.resolve({});
|
2024-02-12 18:32:28 +01:00
|
|
|
|
private static SERVER_URL = "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelect_content";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
protected readonly hasSlotController = new HasSlotController(<LitElement><unknown>this, 'help-text', 'toolbar', 'footer');
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
2024-01-18 00:32:09 +01:00
|
|
|
|
// @ts-ignore different types
|
|
|
|
|
protected _appList : SelectOption[] = this.egw().link_app_list("query") ?? [];
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
// Internal accessors
|
|
|
|
|
get _dialog() : Et2Dialog { return this.shadowRoot.querySelector("et2-dialog");}
|
|
|
|
|
|
|
|
|
|
get _filenameNode() : HTMLInputElement { return this.shadowRoot.querySelector("#filename");}
|
|
|
|
|
|
2024-01-18 00:32:09 +01:00
|
|
|
|
get _fileNodes() : Et2VfsSelectRow[] { return Array.from(this.shadowRoot.querySelectorAll("et2-vfs-select-row"));}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
get _resultNodes() : (HTMLElement & SearchResultElement)[] { return <(HTMLElement & SearchResultElement)[]><unknown>this._fileNodes;}
|
|
|
|
|
|
2024-01-16 16:25:39 +01:00
|
|
|
|
get _searchNode() : HTMLInputElement { return this.shadowRoot.querySelector("#search");}
|
|
|
|
|
|
2024-01-29 17:57:52 +01:00
|
|
|
|
get _pathNode() : Et2VfsPath { return this.shadowRoot.querySelector("#path");}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
get _mimeNode() : Et2Select { return this.shadowRoot.querySelector("#mimeFilter");}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* List of properties that get translated
|
|
|
|
|
* Done separately to not interfere with properties - if we re-define label property,
|
|
|
|
|
* labels go missing.
|
|
|
|
|
*/
|
|
|
|
|
static get translate()
|
|
|
|
|
{
|
|
|
|
|
return {
|
|
|
|
|
...super.translate,
|
|
|
|
|
title: true,
|
|
|
|
|
buttonLabel: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor(parent_egw? : string | IegwAppLocal)
|
|
|
|
|
{
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
|
|
if(parent_egw)
|
|
|
|
|
{
|
|
|
|
|
this._setApiInstance(parent_egw);
|
|
|
|
|
}
|
|
|
|
|
// Use filemanager translations
|
|
|
|
|
this.egw().langRequireApp(this.egw().window, "filemanager", () => {this.requestUpdate()});
|
|
|
|
|
|
2024-02-12 18:32:28 +01:00
|
|
|
|
this.handleClose = this.handleClose.bind(this);
|
2024-01-16 16:25:39 +01:00
|
|
|
|
this.handleCreateDirectory = this.handleCreateDirectory.bind(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
transformAttributes(attr)
|
|
|
|
|
{
|
|
|
|
|
super.transformAttributes(attr);
|
|
|
|
|
|
|
|
|
|
// Start request to get server-side info
|
|
|
|
|
let content = {};
|
|
|
|
|
let attrs = {
|
|
|
|
|
mode: this.mode,
|
|
|
|
|
label: this.buttonLabel,
|
|
|
|
|
path: this.path || null,
|
|
|
|
|
mime: this.mime || null,
|
|
|
|
|
name: this.title
|
|
|
|
|
};
|
2024-01-29 17:57:52 +01:00
|
|
|
|
return this.egw().request(this.egw().link(this.egw().ajaxUrl(this.egw().decodePath(Et2VfsSelectDialog.SERVER_URL))),
|
2024-01-16 16:25:39 +01:00
|
|
|
|
[content, attrs]).then((results) =>
|
|
|
|
|
{
|
|
|
|
|
debugger;
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback()
|
|
|
|
|
{
|
|
|
|
|
super.connectedCallback();
|
|
|
|
|
|
|
|
|
|
if(this.path == "")
|
|
|
|
|
{
|
2024-07-16 00:19:39 +02:00
|
|
|
|
this.path = this.egw().getLocalStorageItem(this.egw().appName, this.constructor.name + "Path") ||
|
|
|
|
|
<string>this.egw()?.preference("startfolder", "filemanager") ||
|
|
|
|
|
"~";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getUpdateComplete()
|
|
|
|
|
{
|
|
|
|
|
const result = await super.getUpdateComplete();
|
|
|
|
|
|
|
|
|
|
// Need to wait for server content
|
|
|
|
|
await this._serverContent;
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 16:49:00 +01:00
|
|
|
|
firstUpdated()
|
|
|
|
|
{
|
|
|
|
|
this._dialog.updateComplete.then(() =>
|
|
|
|
|
{
|
|
|
|
|
this._dialog.panel.style.width = "60em";
|
|
|
|
|
this._dialog.panel.style.height = "40em";
|
|
|
|
|
});
|
2024-03-06 22:25:48 +01:00
|
|
|
|
// Get file list
|
2024-09-17 16:37:02 +02:00
|
|
|
|
if(this.open)
|
|
|
|
|
{
|
|
|
|
|
debugger;
|
|
|
|
|
this.startSearch();
|
|
|
|
|
}
|
2024-02-23 16:49:00 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-18 00:32:09 +01:00
|
|
|
|
protected willUpdate(changedProperties : PropertyValues)
|
|
|
|
|
{
|
|
|
|
|
super.willUpdate(changedProperties);
|
|
|
|
|
|
|
|
|
|
if(changedProperties.has("mode"))
|
|
|
|
|
{
|
|
|
|
|
this.multiple = this.mode == "open-multiple";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(changedProperties.has("path"))
|
|
|
|
|
{
|
|
|
|
|
this.startSearch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 16:25:39 +01:00
|
|
|
|
public setPath(path)
|
|
|
|
|
{
|
2024-05-24 23:22:51 +02:00
|
|
|
|
const oldValue = this.path;
|
|
|
|
|
|
|
|
|
|
// Selection doesn't stay across sub-dirs. Notify user we dropped them.
|
|
|
|
|
if(this.value.length && path != oldValue)
|
|
|
|
|
{
|
|
|
|
|
const length = this.value.length;
|
2024-11-18 21:45:56 +01:00
|
|
|
|
this.value.length = 0;
|
2024-05-24 23:22:51 +02:00
|
|
|
|
this.updateComplete.then(() =>
|
|
|
|
|
{
|
|
|
|
|
render(html`
|
|
|
|
|
<sl-alert duration="5000" closable open>
|
|
|
|
|
<sl-icon slot="icon" name="info-circle"></sl-icon>
|
|
|
|
|
${this.egw().lang("Selection of files can only be done in one folder. %1 files unselected.", length)}
|
|
|
|
|
</sl-alert>`, <HTMLElement><unknown>this);
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
if(path == '..')
|
|
|
|
|
{
|
|
|
|
|
path = this.dirname(this.path);
|
|
|
|
|
}
|
|
|
|
|
this._pathNode.value = this.path = path;
|
|
|
|
|
this.requestUpdate("path", oldValue);
|
2024-02-23 00:21:28 +01:00
|
|
|
|
this.currentResult = null;
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
return this._searchPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get directory of a path
|
|
|
|
|
*
|
|
|
|
|
* @param {string} _path
|
|
|
|
|
* @returns string
|
|
|
|
|
*/
|
|
|
|
|
public dirname(_path)
|
|
|
|
|
{
|
|
|
|
|
let parts = _path.split('/');
|
|
|
|
|
parts.pop();
|
|
|
|
|
return parts.join('/') || '/';
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-02 23:20:33 +01:00
|
|
|
|
/**
|
|
|
|
|
* Get file information of currently displayed paths
|
|
|
|
|
*
|
|
|
|
|
* Returns null if the path is not currently displayed
|
|
|
|
|
* @param _path
|
|
|
|
|
*/
|
|
|
|
|
public fileInfo(_path)
|
|
|
|
|
{
|
2024-02-23 00:21:28 +01:00
|
|
|
|
return this._searchResults.find(f => f.path == _path);
|
2024-02-02 23:20:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 16:25:39 +01:00
|
|
|
|
/**
|
|
|
|
|
* Shows the dialog.
|
|
|
|
|
*/
|
|
|
|
|
public show()
|
|
|
|
|
{
|
|
|
|
|
this.open = true;
|
2024-02-23 00:21:28 +01:00
|
|
|
|
if(this.path && this._searchResults.length == 0)
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
|
|
|
|
this.startSearch();
|
|
|
|
|
}
|
|
|
|
|
return Promise.all([
|
2024-01-18 00:32:09 +01:00
|
|
|
|
this.updateComplete,
|
2024-03-06 22:25:48 +01:00
|
|
|
|
this._searchPromise
|
2024-02-07 00:16:00 +01:00
|
|
|
|
]).then(() =>
|
2024-03-06 22:25:48 +01:00
|
|
|
|
{
|
|
|
|
|
return this._dialog.show();
|
|
|
|
|
}).then(() =>
|
2024-02-07 00:16:00 +01:00
|
|
|
|
{
|
|
|
|
|
// Set current file to first value
|
|
|
|
|
if(this.value && this.value[0])
|
|
|
|
|
{
|
2024-02-26 22:26:14 +01:00
|
|
|
|
this.setCurrentResult(this._fileNodes.find(node => node.value.path == this.value[0]));
|
2024-02-07 00:16:00 +01:00
|
|
|
|
}
|
|
|
|
|
});
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hides the dialog.
|
|
|
|
|
*/
|
|
|
|
|
public hide()
|
|
|
|
|
{
|
|
|
|
|
this.open = false;
|
|
|
|
|
return this._dialog.hide();
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-12 18:32:28 +01:00
|
|
|
|
async getComplete() : Promise<[number, Object]>
|
2024-01-18 00:32:09 +01:00
|
|
|
|
{
|
2024-03-06 22:25:48 +01:00
|
|
|
|
await this.updateComplete;
|
2024-02-12 18:32:28 +01:00
|
|
|
|
const value = await this._dialog.getComplete();
|
|
|
|
|
await this.handleClose();
|
|
|
|
|
value[1] = this.value;
|
|
|
|
|
return value;
|
2024-01-18 00:32:09 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-20 21:20:43 +01:00
|
|
|
|
protected localSearch<FileInfo>(search : string, searchOptions : object, localOptions : FileInfo[] = []) : Promise<FileInfo[]>
|
|
|
|
|
{
|
|
|
|
|
return super.localSearch(search, {...searchOptions, mime: this.mime}, localOptions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public searchMatch<FileInfo>(search : string, searchOptions : Object, option : FileInfo) : boolean
|
|
|
|
|
{
|
|
|
|
|
let result = super.searchMatch(search, searchOptions, option);
|
|
|
|
|
|
|
|
|
|
// Add in local mime check
|
|
|
|
|
if(result && searchOptions.mime)
|
|
|
|
|
{
|
|
|
|
|
result = result && option.mime.match(searchOptions.mime);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
remoteSearch<FileInfo>(search : string, options : object) : Promise<FileInfo[]>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
|
|
|
|
// Include a limit, even if options don't, to avoid massive lists breaking the UI
|
|
|
|
|
let sendOptions = {
|
2024-01-18 00:32:09 +01:00
|
|
|
|
path: this.path,
|
|
|
|
|
mime: this.mime,
|
2024-01-16 16:25:39 +01:00
|
|
|
|
num_rows: 100,
|
|
|
|
|
...options
|
|
|
|
|
}
|
2024-02-23 00:21:28 +01:00
|
|
|
|
return super.remoteSearch(search, sendOptions);
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
processRemoteResults<FileInfo>(results) : FileInfo[]
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
2024-02-23 00:21:28 +01:00
|
|
|
|
const result = super.processRemoteResults(results);
|
2024-02-07 00:16:00 +01:00
|
|
|
|
if(typeof results.path === "string")
|
|
|
|
|
{
|
|
|
|
|
// Something like a redirect or link followed - server is sending us a "corrected" path
|
2024-02-09 17:15:11 +01:00
|
|
|
|
this.path = results.path;
|
|
|
|
|
}
|
|
|
|
|
if(typeof results.writable !== "undefined")
|
|
|
|
|
{
|
|
|
|
|
this._pathWritable = results.writable;
|
|
|
|
|
this.requestUpdate("_pathWritable");
|
2024-02-07 00:16:00 +01:00
|
|
|
|
}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
this.helpText = results?.message ?? "";
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
return result;
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Inject application specific egw object with loaded translations into the dialog
|
|
|
|
|
*
|
|
|
|
|
* @param {string|egw} _egw_or_appname egw object with already loaded translations or application name to load translations for
|
|
|
|
|
*/
|
|
|
|
|
_setApiInstance(_egw_or_appname ? : string | IegwAppLocal)
|
|
|
|
|
{
|
|
|
|
|
if(typeof _egw_or_appname == 'undefined')
|
|
|
|
|
{
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
_egw_or_appname = egw_appName;
|
|
|
|
|
}
|
|
|
|
|
// if egw object is passed in because called from et2, just use it
|
|
|
|
|
if(typeof _egw_or_appname != 'string')
|
|
|
|
|
{
|
|
|
|
|
this.__egw = _egw_or_appname;
|
|
|
|
|
}
|
|
|
|
|
// otherwise use given appname to create app-specific egw instance and load default translations
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
this.__egw = egw(_egw_or_appname);
|
|
|
|
|
this.egw().langRequireApp(this.egw().window, _egw_or_appname);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-12 18:32:28 +01:00
|
|
|
|
private async handleClose()
|
|
|
|
|
{
|
|
|
|
|
// Should already be complete, we want the button
|
|
|
|
|
let dialogValue = await this._dialog.getComplete();
|
|
|
|
|
switch(this.mode)
|
|
|
|
|
{
|
|
|
|
|
case "select-dir":
|
|
|
|
|
// If they didn't pick a specific directory and didn't cancel, use the current directory
|
2024-11-18 21:45:56 +01:00
|
|
|
|
if(this.value.length == 0)
|
|
|
|
|
{
|
|
|
|
|
this.value.splice(0, 0, this.path)
|
|
|
|
|
}
|
2024-02-12 18:32:28 +01:00
|
|
|
|
break;
|
|
|
|
|
case "saveas":
|
|
|
|
|
// Saveas wants a full path, including filename
|
2024-11-18 21:45:56 +01:00
|
|
|
|
this.value.splice(0, this.value.length, this.path + "/" + this._filenameNode.value ?? this.filename);
|
2024-02-12 18:32:28 +01:00
|
|
|
|
|
|
|
|
|
// Check for existing file, ask what to do
|
|
|
|
|
if(this.fileInfo(this.value[0]))
|
|
|
|
|
{
|
2024-03-01 22:37:53 +01:00
|
|
|
|
let result = await this.overwritePrompt(this._filenameNode ?? this.filename);
|
2024-02-12 18:32:28 +01:00
|
|
|
|
if(result == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-11-18 21:45:56 +01:00
|
|
|
|
this.value.splice(0, this.value.length, this.path + "/" + result);
|
2024-02-12 18:32:28 +01:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-07-16 00:19:39 +02:00
|
|
|
|
|
|
|
|
|
// Save path for next time
|
|
|
|
|
this.egw().setLocalStorageItem(this.egw().appName, this.constructor.name + "Path", this.path);
|
2024-02-12 18:32:28 +01:00
|
|
|
|
this.dispatchEvent(new Event("change", {bubbles: true}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* User tried to saveas when we can see that file already exists. Prompt to overwrite or rename.
|
|
|
|
|
*
|
|
|
|
|
* We offer a suggested new name by appending "(#)", and give back either the original filename, their
|
|
|
|
|
* modified filename, or null if they cancel.
|
|
|
|
|
*
|
|
|
|
|
* @param filename
|
|
|
|
|
* @returns {Promise<[number|string, Object]|null>} [Button,filename] or null if they cancel
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
private overwritePrompt(filename) : Promise<[number | string, object] | null>
|
|
|
|
|
{
|
|
|
|
|
// Make a filename suggestion
|
|
|
|
|
const parts = filename.split(".");
|
|
|
|
|
const extension = parts.pop();
|
|
|
|
|
const newName = parts.join(".");
|
|
|
|
|
let counter = 0;
|
|
|
|
|
let suggestion;
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
counter++;
|
|
|
|
|
suggestion = `${newName} (${counter}).${extension}`;
|
|
|
|
|
}
|
|
|
|
|
while(this.fileInfo(suggestion))
|
|
|
|
|
|
|
|
|
|
// Ask about it
|
|
|
|
|
const saveModeDialogButtons = [
|
|
|
|
|
{
|
|
|
|
|
label: self.egw().lang("Yes"),
|
|
|
|
|
id: "overwrite",
|
|
|
|
|
class: "ui-priority-primary",
|
|
|
|
|
"default": true,
|
|
|
|
|
image: 'check'
|
|
|
|
|
},
|
|
|
|
|
{label: self.egw().lang("Rename"), id: "rename", image: 'edit'},
|
|
|
|
|
{label: self.egw().lang("Cancel"), id: "cancel"}
|
|
|
|
|
];
|
|
|
|
|
return Et2Dialog.show_prompt(null,
|
|
|
|
|
self.egw().lang('Do you want to overwrite existing file %1 in directory %2?', filename, this.path),
|
|
|
|
|
self.egw().lang('File %1 already exists', filename),
|
|
|
|
|
suggestion, saveModeDialogButtons, null).getComplete().then(([button, value]) =>
|
|
|
|
|
{
|
|
|
|
|
if(button == "cancel")
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return button == "rename" ? value.value : filename;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-18 00:32:09 +01:00
|
|
|
|
/**
|
|
|
|
|
* Sets the selected files
|
|
|
|
|
* @param {Et2VfsSelectRow | Et2VfsSelectRow[]} file
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
private setSelectedFiles(file : Et2VfsSelectRow | Et2VfsSelectRow[])
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
2024-01-18 00:32:09 +01:00
|
|
|
|
const newSelectedOptions = Array.isArray(file) ? file : [file];
|
|
|
|
|
|
|
|
|
|
// Clear existing selection
|
|
|
|
|
this._fileNodes.forEach(el =>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
2024-01-18 00:32:09 +01:00
|
|
|
|
el.selected = false;
|
|
|
|
|
el.requestUpdate("selected");
|
|
|
|
|
});
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
2024-01-18 00:32:09 +01:00
|
|
|
|
// Set the new selection
|
|
|
|
|
if(newSelectedOptions.length)
|
|
|
|
|
{
|
|
|
|
|
newSelectedOptions.forEach(el =>
|
|
|
|
|
{
|
|
|
|
|
el.selected = true;
|
|
|
|
|
el.requestUpdate("selected");
|
|
|
|
|
});
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
2024-01-18 00:32:09 +01:00
|
|
|
|
|
|
|
|
|
// Update selection, value, and display label
|
2024-02-26 22:26:14 +01:00
|
|
|
|
this.searchResultSelected();
|
2024-01-18 00:32:09 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-09 00:32:31 +01:00
|
|
|
|
/**
|
|
|
|
|
* Toggles a search result's selected state
|
|
|
|
|
*/
|
|
|
|
|
protected toggleResultSelection(result : HTMLElement & SearchResultElement, force? : boolean)
|
|
|
|
|
{
|
|
|
|
|
if(!this.multiple)
|
|
|
|
|
{
|
|
|
|
|
this._resultNodes.forEach(n =>
|
|
|
|
|
{
|
|
|
|
|
n.selected = false;
|
|
|
|
|
n.requestUpdate("selected");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
super.toggleResultSelection(result, force);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-18 00:32:09 +01:00
|
|
|
|
/**
|
|
|
|
|
* This method must be called whenever the selection changes. It will update the selected file cache, the current
|
|
|
|
|
* value, and the display value
|
|
|
|
|
*/
|
2024-02-23 00:21:28 +01:00
|
|
|
|
protected searchResultSelected()
|
2024-01-18 00:32:09 +01:00
|
|
|
|
{
|
2024-02-23 00:21:28 +01:00
|
|
|
|
super.searchResultSelected();
|
2024-01-18 00:32:09 +01:00
|
|
|
|
|
|
|
|
|
// Update the value
|
|
|
|
|
if(this.multiple)
|
|
|
|
|
{
|
2024-11-18 21:45:56 +01:00
|
|
|
|
this.value.splice(0, this.value.length, ...this.selectedResults.map(el => el.value.path));
|
2024-01-18 00:32:09 +01:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2024-11-18 21:45:56 +01:00
|
|
|
|
this.value.splice(0, this.value.length, ...(this.selectedResults?.length ? [this.selectedResults[0].value.path] : []) ?? []);
|
2024-01-18 00:32:09 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new directory in the current one
|
|
|
|
|
* @param {MouseEvent | KeyboardEvent} event
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
protected async handleCreateDirectory(event : MouseEvent | KeyboardEvent)
|
|
|
|
|
{
|
|
|
|
|
// Get new directory name
|
|
|
|
|
let [button, value] = await Et2Dialog.show_prompt(
|
|
|
|
|
null, this.egw().lang('New directory'), this.egw().lang('Create directory')
|
|
|
|
|
).getComplete();
|
|
|
|
|
let dir = value.value;
|
|
|
|
|
|
|
|
|
|
if(button && dir)
|
|
|
|
|
{
|
|
|
|
|
this.egw().request('EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_create_dir', [dir, this.path])
|
|
|
|
|
.then((msg) =>
|
|
|
|
|
{
|
|
|
|
|
this.egw().message(msg);
|
|
|
|
|
this.setPath(this.path + '/' + dir);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-29 18:03:44 +01:00
|
|
|
|
/**
|
|
|
|
|
* SearchMixin handles the actual selection, we just reject directories here.
|
|
|
|
|
*
|
|
|
|
|
* @param {MouseEvent} event
|
|
|
|
|
*/
|
2024-01-18 00:32:09 +01:00
|
|
|
|
handleFileClick(event : MouseEvent)
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
2024-01-18 00:32:09 +01:00
|
|
|
|
const target = event.target as HTMLElement;
|
|
|
|
|
const file : Et2VfsSelectRow = target.closest('et2-vfs-select-row');
|
|
|
|
|
const oldValue = this.value;
|
|
|
|
|
|
|
|
|
|
if(file && !file.disabled)
|
|
|
|
|
{
|
2024-03-21 16:30:58 +01:00
|
|
|
|
// Can't select a directory normally
|
2024-07-16 00:19:39 +02:00
|
|
|
|
if(file.value.isDir)
|
2024-01-18 00:32:09 +01:00
|
|
|
|
{
|
2024-07-16 00:19:39 +02:00
|
|
|
|
this.setPath(file.value.path);
|
2024-02-29 18:03:44 +01:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
2024-01-18 00:32:09 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
2024-03-21 16:30:58 +01:00
|
|
|
|
// can't select anything in "saveas", but set the file name
|
|
|
|
|
else if(this.mode == "saveas")
|
|
|
|
|
{
|
|
|
|
|
this._filenameNode.value = file.value.name;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
this.updateComplete.then(() => this._filenameNode.focus());
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-01-18 00:32:09 +01:00
|
|
|
|
// Set focus after updating so the value is announced by screen readers
|
|
|
|
|
//this.updateComplete.then(() => this.displayInput.focus({ preventScroll: true }));
|
|
|
|
|
}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 00:16:00 +01:00
|
|
|
|
handleKeyDown(event)
|
|
|
|
|
{
|
|
|
|
|
// Ignore selects
|
|
|
|
|
if(event.target.tagName.startsWith('ET2-SELECT'))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Grab any keypresses, avoid EgwAction reacting on them too
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
|
|
|
|
// Navigate options
|
|
|
|
|
if(["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key))
|
|
|
|
|
{
|
|
|
|
|
const files = this._fileNodes;
|
2024-02-23 00:21:28 +01:00
|
|
|
|
const currentIndex = files.indexOf(this.currentResult);
|
2024-02-07 00:16:00 +01:00
|
|
|
|
let newIndex = Math.max(0, currentIndex);
|
|
|
|
|
|
|
|
|
|
// Prevent scrolling
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
if(event.key === "ArrowDown")
|
|
|
|
|
{
|
|
|
|
|
newIndex = currentIndex + 1;
|
|
|
|
|
if(newIndex > files.length - 1)
|
|
|
|
|
{
|
|
|
|
|
return this._mimeNode.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if(event.key === "ArrowUp")
|
|
|
|
|
{
|
|
|
|
|
newIndex = currentIndex - 1;
|
|
|
|
|
if(newIndex < 0)
|
|
|
|
|
{
|
|
|
|
|
return this._pathNode.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if(event.key === "Home")
|
|
|
|
|
{
|
|
|
|
|
newIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
else if(event.key === "End")
|
|
|
|
|
{
|
|
|
|
|
newIndex = files.length - 1;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
this.setCurrentResult(files[newIndex]);
|
2024-02-07 00:16:00 +01:00
|
|
|
|
}
|
2024-02-23 00:21:28 +01:00
|
|
|
|
else if([" "].includes(event.key) && this.currentResult)
|
2024-02-07 00:16:00 +01:00
|
|
|
|
{
|
|
|
|
|
// Prevent scrolling
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
return this.handleFileClick(event);
|
|
|
|
|
}
|
2024-02-23 00:21:28 +01:00
|
|
|
|
else if(["Enter"].includes(event.key) && this.currentResult && !this.currentResult.disabled)
|
2024-02-07 00:16:00 +01:00
|
|
|
|
{
|
2024-07-16 00:19:39 +02:00
|
|
|
|
return this.handleFileClick(event);
|
2024-02-07 00:16:00 +01:00
|
|
|
|
}
|
|
|
|
|
else if(["Escape"].includes(event.key))
|
|
|
|
|
{
|
|
|
|
|
this.open = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 16:25:39 +01:00
|
|
|
|
handleSearchKeyDown(event)
|
|
|
|
|
{
|
|
|
|
|
clearTimeout(this._searchTimeout);
|
|
|
|
|
|
|
|
|
|
// Up / Down navigates options
|
2024-02-23 00:21:28 +01:00
|
|
|
|
if(['ArrowDown', 'ArrowUp'].includes(event.key) && this._searchResults.length)
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
2024-02-23 00:21:28 +01:00
|
|
|
|
return super.handleSearchKeyDown(event);
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
// Start search immediately
|
|
|
|
|
else if(event.key == "Enter")
|
|
|
|
|
{
|
2024-02-23 00:21:28 +01:00
|
|
|
|
return super.handleSearchKeyDown(event);
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
else if(event.key == "Escape")
|
|
|
|
|
{
|
2024-02-23 00:21:28 +01:00
|
|
|
|
super.handleSearchKeyDown(event);
|
|
|
|
|
|
2024-01-18 00:32:09 +01:00
|
|
|
|
event.preventDefault();
|
2024-11-18 21:45:56 +01:00
|
|
|
|
this.value.length = 0;
|
2024-01-18 00:32:09 +01:00
|
|
|
|
this.hide();
|
2024-01-16 16:25:39 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-21 17:28:06 +01:00
|
|
|
|
// Start the search automatically if they have something typed
|
|
|
|
|
if(this._searchNode.value.length > 0)
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
2024-01-29 17:57:52 +01:00
|
|
|
|
this._searchTimeout = window.setTimeout(() => {this.startSearch()}, Et2VfsSelectDialog.SEARCH_TIMEOUT);
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected toolbarTemplate() : TemplateResult
|
|
|
|
|
{
|
|
|
|
|
return html`
|
2024-02-23 16:49:00 +01:00
|
|
|
|
<div class="et2_toolbar">
|
2024-01-16 16:25:39 +01:00
|
|
|
|
<et2-button statustext="Go to your home directory" id="home"
|
|
|
|
|
image="filemanager/gohome"
|
|
|
|
|
aria-label=${this.egw().lang("Go to your home folder")}
|
|
|
|
|
noSubmit="true"
|
|
|
|
|
@click=${() => this.setPath("~")}
|
|
|
|
|
></et2-button>
|
|
|
|
|
<et2-button statustext="Up" id="up"
|
|
|
|
|
image="filemanager/goup" noSubmit="true" aria-label=${this.egw().lang("Up")}
|
|
|
|
|
@click=${() => this.setPath("..")}
|
|
|
|
|
>
|
|
|
|
|
</et2-button>
|
|
|
|
|
<et2-button statustext="Favorites" id="favorites"
|
|
|
|
|
aria-label=${this.egw().lang("Favorites")}
|
|
|
|
|
image="filemanager/fav_filter" noSubmit="true"
|
|
|
|
|
@click=${() => this.setPath("/apps/favorites")}
|
|
|
|
|
></et2-button>
|
2024-05-24 23:22:51 +02:00
|
|
|
|
<et2-select id="app" emptyLabel="${this.egw().lang("Applications")}" noLang="1"
|
2024-01-29 17:57:52 +01:00
|
|
|
|
.select_options=${this._appList}
|
2024-01-18 00:32:09 +01:00
|
|
|
|
@change=${(e) => this.setPath("/apps/" + e.target.value)}
|
|
|
|
|
>
|
|
|
|
|
</et2-select>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
<et2-button statustext="Create directory" id="createdir" class="createDir"
|
|
|
|
|
arial-label=${this.egw().lang("Create directory")}
|
2024-02-09 17:15:11 +01:00
|
|
|
|
?disabled=${!this._pathWritable}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
noSubmit="true"
|
|
|
|
|
image="filemanager/button_createdir"
|
|
|
|
|
roImage="filemanager/createdir_disabled"
|
|
|
|
|
@click=${this.handleCreateDirectory}
|
|
|
|
|
></et2-button>
|
|
|
|
|
<file id="upload_file" statustext="upload file" progress_dropdownlist="true" multiple="true"
|
2024-02-09 17:15:11 +01:00
|
|
|
|
?disabled=${!this._pathWritable}
|
|
|
|
|
onFinish="app.vfsSelectUI.storeFile"
|
|
|
|
|
></file>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
<et2-searchbox id="search"
|
|
|
|
|
@keydown=${this.handleSearchKeyDown}
|
2024-01-18 00:32:09 +01:00
|
|
|
|
@sl-clear=${this.startSearch}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
></et2-searchbox>
|
2024-02-23 16:49:00 +01:00
|
|
|
|
</div>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
protected resultTemplate(file : FileInfo, index)
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
2024-02-23 00:21:28 +01:00
|
|
|
|
const classes = file.class ? Object.fromEntries((file.class).split(" ").map(k => [k, true])) : {};
|
2024-01-18 00:32:09 +01:00
|
|
|
|
|
2024-01-16 16:25:39 +01:00
|
|
|
|
return html`
|
2024-02-23 00:21:28 +01:00
|
|
|
|
<et2-vfs-select-row
|
|
|
|
|
class=${classMap({
|
|
|
|
|
...classes
|
|
|
|
|
})}
|
|
|
|
|
?disabled=${file.disabled || this.mode == "select-dir" && !file.isDir}
|
|
|
|
|
.selected=${this.value.includes(file.path)}
|
|
|
|
|
.value=${file}
|
|
|
|
|
@mouseup=${this.handleFileClick}
|
|
|
|
|
></et2-vfs-select-row>`;
|
2024-01-18 00:32:09 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 00:21:28 +01:00
|
|
|
|
protected noResultsTemplate() : TemplateResult
|
2024-01-18 00:32:09 +01:00
|
|
|
|
{
|
|
|
|
|
return html`
|
2024-02-23 00:21:28 +01:00
|
|
|
|
<div class="search__empty vfs_select__empty">
|
2024-01-18 00:32:09 +01:00
|
|
|
|
<et2-image src="filemanager"></et2-image>
|
|
|
|
|
${this.egw().lang("no files in this directory.")}
|
|
|
|
|
</div>`;
|
2024-01-16 16:25:39 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected mimeOptionsTemplate()
|
|
|
|
|
{
|
|
|
|
|
return html``;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected footerTemplate()
|
|
|
|
|
{
|
|
|
|
|
let image = "check";
|
|
|
|
|
switch(this.mode)
|
|
|
|
|
{
|
|
|
|
|
case "saveas":
|
|
|
|
|
image = "save_new";
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const buttons = [
|
2024-02-02 23:20:33 +01:00
|
|
|
|
{id: "ok", label: this.buttonLabel, image: image, button_id: Et2Dialog.OK_BUTTON},
|
2024-05-24 23:22:51 +02:00
|
|
|
|
{id: "cancel", label: this.egw().lang("cancel"), image: "cancel", button_id: Et2Dialog.CANCEL_BUTTON}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return html`
|
2024-02-09 16:47:04 +01:00
|
|
|
|
<slot name="footer" slot="footer"></slot>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
${repeat(buttons, (button : DialogButton) => button.id, (button, index) =>
|
|
|
|
|
{
|
2024-02-09 16:47:04 +01:00
|
|
|
|
// style=order is to allow slotted buttons an opportunity to choose where they go.
|
|
|
|
|
// Default is they'll go before our primary button
|
2024-01-16 16:25:39 +01:00
|
|
|
|
return html`
|
|
|
|
|
<et2-button id=${button.id}
|
2024-02-02 23:20:33 +01:00
|
|
|
|
button_id=${button.button_id}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
class="et2_button et2_vfs__button"
|
2024-02-09 16:47:04 +01:00
|
|
|
|
style="order: ${(index + 1) * 2}"
|
2024-01-16 16:25:39 +01:00
|
|
|
|
label=${button.label}
|
|
|
|
|
variant=${index == 0 ? "primary" : "default"}
|
|
|
|
|
slot="footer"
|
|
|
|
|
.image=${ifDefined(button.image)}
|
|
|
|
|
.noSubmit=${true}
|
|
|
|
|
>${button.label}
|
|
|
|
|
</et2-button>
|
|
|
|
|
`
|
|
|
|
|
})}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render()
|
|
|
|
|
{
|
|
|
|
|
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
|
|
|
|
const hasFooterSlot = this.hasSlotController.test('footer');
|
|
|
|
|
const hasToolbarSlot = this.hasSlotController.test('toolbar');
|
|
|
|
|
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
|
|
|
|
const hasToolbar = !!hasToolbarSlot;
|
|
|
|
|
|
|
|
|
|
const hasFilename = this.mode == "saveas";
|
2024-02-12 18:32:28 +01:00
|
|
|
|
const mime = typeof this.mime == "string" ? this.mime : (this.mimeList.length == 1 ? this.mimeList[0].value : "");
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<et2-dialog
|
2024-01-18 00:32:09 +01:00
|
|
|
|
.isModal=${true}
|
|
|
|
|
.destroyOnClose=${false}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
.title=${this.title}
|
|
|
|
|
.open=${this.open}
|
2024-02-07 00:16:00 +01:00
|
|
|
|
@keydown=${this.handleKeyDown}
|
2024-02-12 18:32:28 +01:00
|
|
|
|
@close=${this.handleClose}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
>
|
2024-02-12 18:32:28 +01:00
|
|
|
|
${hasFilename ? html`
|
|
|
|
|
<et2-textbox id="filename"
|
|
|
|
|
.value=${this.filename}
|
|
|
|
|
@change=${(e) => {this.filename = e.target.value;}}
|
|
|
|
|
>
|
|
|
|
|
</et2-textbox>` : nothing}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
<div
|
|
|
|
|
part="toolbar"
|
|
|
|
|
id="toolbar"
|
|
|
|
|
class="vfs_select__toolbar"
|
|
|
|
|
>
|
|
|
|
|
<slot name="prefix"></slot>
|
|
|
|
|
<slot name="toolbar">${hasToolbar ? nothing : this.toolbarTemplate()}</slot>
|
|
|
|
|
<slot name="suffix"></slot>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
part="path"
|
|
|
|
|
>
|
2024-01-29 17:57:52 +01:00
|
|
|
|
<et2-vfs-path id="path"
|
|
|
|
|
.value=${this.path}
|
|
|
|
|
@change=${() => {this.setPath(this._pathNode.value)}}
|
|
|
|
|
></et2-vfs-path>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
</div>
|
2024-02-23 00:21:28 +01:00
|
|
|
|
${this.searchResultsTemplate()}
|
2024-05-24 23:22:51 +02:00
|
|
|
|
<slot></slot>
|
2024-01-16 16:25:39 +01:00
|
|
|
|
<sl-visually-hidden>
|
|
|
|
|
<et2-label for="mimeFilter">${this.egw().lang("mime filter")}</et2-label>
|
|
|
|
|
</sl-visually-hidden>
|
|
|
|
|
<et2-select
|
|
|
|
|
id="mimeFilter"
|
|
|
|
|
part="mimefilter"
|
|
|
|
|
class="vfs_select__mimefilter"
|
|
|
|
|
?readonly=${this.mimeList.length == 1}
|
2024-01-22 23:19:21 +01:00
|
|
|
|
.emptyLabel=${this.egw().lang("All files")}
|
|
|
|
|
.select_options=${this.mimeList}
|
|
|
|
|
.value=${mime}
|
|
|
|
|
@change=${(e) =>
|
|
|
|
|
{
|
|
|
|
|
this.mime = e.target.value;
|
|
|
|
|
this.startSearch();
|
|
|
|
|
}}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
>
|
|
|
|
|
${this.mimeOptionsTemplate()}
|
|
|
|
|
</et2-select>
|
|
|
|
|
<div
|
|
|
|
|
part="form-control-help-text"
|
|
|
|
|
id="help-text"
|
|
|
|
|
class="form-control__help-text"
|
|
|
|
|
aria-hidden=${hasHelpText ? 'false' : 'true'}
|
|
|
|
|
>
|
|
|
|
|
<slot name="help-text">${this.helpText}</slot>
|
|
|
|
|
</div>
|
2024-02-09 16:47:04 +01:00
|
|
|
|
${this.footerTemplate()}
|
2024-01-16 16:25:39 +01:00
|
|
|
|
</et2-dialog>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-29 17:57:52 +01:00
|
|
|
|
customElements.define("et2-vfs-select-dialog", Et2VfsSelectDialog);
|
2024-01-16 16:25:39 +01:00
|
|
|
|
|
2024-03-20 21:20:43 +01:00
|
|
|
|
export type FileInfo = SearchResult &
|
2024-01-16 16:25:39 +01:00
|
|
|
|
{
|
|
|
|
|
mime : string,
|
|
|
|
|
isDir : boolean,
|
2024-02-23 00:21:28 +01:00
|
|
|
|
// Full VFS path
|
2024-01-16 16:25:39 +01:00
|
|
|
|
path? : string,
|
2024-02-02 23:20:33 +01:00
|
|
|
|
// Direct download link
|
|
|
|
|
downloadUrl? : string
|
2024-02-23 00:21:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* We expect the server to respond with file data in this format
|
|
|
|
|
*/
|
|
|
|
|
interface FileResultsInterface extends SearchResultsInterface<FileInfo>
|
|
|
|
|
{
|
|
|
|
|
// Something like a redirect or link followed - server is sending us a "corrected" path
|
|
|
|
|
path? : string,
|
|
|
|
|
// The current directory is not writable
|
|
|
|
|
writable? : boolean
|
|
|
|
|
}
|