mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-06-25 20:31:31 +02:00
WIP Et2VfsSelect: Now files showing up in list
This commit is contained in:
parent
e014487e86
commit
faeee31155
@ -8,6 +8,7 @@ export default css`
|
|||||||
et2-dialog::part(body) {
|
et2-dialog::part(body) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
max-height: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vfs_select__listbox {
|
.vfs_select__listbox {
|
||||||
@ -19,6 +20,7 @@ export default css`
|
|||||||
.vfs_select__listbox .vfs_select__empty {
|
.vfs_select__listbox .vfs_select__empty {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 5em;
|
min-height: 5em;
|
||||||
|
min-width: 20em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -26,6 +28,18 @@ export default css`
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vfs_select__file_row {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfs_select__listbox .vfs_select__loading {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 15em; // 3 * listbox min height
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfs_select__listbox sl-spinner {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
.vfs_select__listbox .vfs_select__empty et2-image {
|
.vfs_select__listbox .vfs_select__empty et2-image {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||||
import {html, LitElement, nothing, TemplateResult} from "lit";
|
import {html, LitElement, nothing, PropertyValues, TemplateResult} from "lit";
|
||||||
import shoelace from "../Styles/shoelace";
|
import shoelace from "../Styles/shoelace";
|
||||||
import styles from "./Et2VfsSelect.styles";
|
import styles from "./Et2VfsSelect.styles";
|
||||||
import {property} from "lit/decorators/property.js";
|
import {property} from "lit/decorators/property.js";
|
||||||
@ -22,6 +22,7 @@ import {DialogButton, Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
|||||||
import {HasSlotController} from "../Et2Widget/slot";
|
import {HasSlotController} from "../Et2Widget/slot";
|
||||||
import {IegwAppLocal} from "../../jsapi/egw_global";
|
import {IegwAppLocal} from "../../jsapi/egw_global";
|
||||||
import {Et2Select} from "../Et2Select/Et2Select";
|
import {Et2Select} from "../Et2Select/Et2Select";
|
||||||
|
import {Et2VfsSelectRow} from "./Et2VfsSelectRow";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Select files (including directories) from the VFS
|
* @summary Select files (including directories) from the VFS
|
||||||
@ -29,6 +30,7 @@ import {Et2Select} from "../Et2Select/Et2Select";
|
|||||||
*
|
*
|
||||||
* @dependency et2-dialog
|
* @dependency et2-dialog
|
||||||
* @dependency et2-select
|
* @dependency et2-select
|
||||||
|
* @dependency et2-vfs-select-row
|
||||||
*
|
*
|
||||||
* @slot title - Optional additions to title. Works best with `et2-button-icon`.
|
* @slot title - Optional additions to title. Works best with `et2-button-icon`.
|
||||||
* @slot toolbar - Toolbar containing controls for search & navigation
|
* @slot toolbar - Toolbar containing controls for search & navigation
|
||||||
@ -70,10 +72,10 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
* Dialog mode
|
* Dialog mode
|
||||||
* Quickly sets button label, multiple, selection and for "select-dir", mime-type
|
* Quickly sets button label, multiple, selection and for "select-dir", mime-type
|
||||||
**/
|
**/
|
||||||
@property() mode : "open" | "open-multiple" | "saveas" | "select-dir" = "open";
|
@property({type: String}) mode : "open" | "open-multiple" | "saveas" | "select-dir";
|
||||||
|
|
||||||
/** Button label */
|
/** Button label */
|
||||||
@property() buttonLabel : string = "Select";
|
@property({type: String}) buttonLabel : string = "Select";
|
||||||
|
|
||||||
/** Provide a suggested filename for saving */
|
/** Provide a suggested filename for saving */
|
||||||
@property() filename : string = "";
|
@property() filename : string = "";
|
||||||
@ -95,7 +97,9 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
|
|
||||||
@state() searching = false;
|
@state() searching = false;
|
||||||
@state() open : boolean = false;
|
@state() open : boolean = false;
|
||||||
@state() currentFile;
|
@state() currentFile : Et2VfsSelectRow;
|
||||||
|
@state() selectedFiles : Et2VfsSelectRow[] = [];
|
||||||
|
|
||||||
|
|
||||||
// SearchMixinInterface //
|
// SearchMixinInterface //
|
||||||
@property() searchUrl : string = "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelectFiles";
|
@property() searchUrl : string = "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelectFiles";
|
||||||
@ -117,14 +121,16 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
|
|
||||||
protected readonly hasSlotController = new HasSlotController(this, 'help-text', 'toolbar', 'footer');
|
protected readonly hasSlotController = new HasSlotController(this, 'help-text', 'toolbar', 'footer');
|
||||||
|
|
||||||
protected _fileList = [];
|
protected _fileList : FileInfo[] = [];
|
||||||
|
// @ts-ignore different types
|
||||||
|
protected _appList : SelectOption[] = this.egw().link_app_list("query") ?? [];
|
||||||
|
|
||||||
// Internal accessors
|
// Internal accessors
|
||||||
get _dialog() : Et2Dialog { return this.shadowRoot.querySelector("et2-dialog");}
|
get _dialog() : Et2Dialog { return this.shadowRoot.querySelector("et2-dialog");}
|
||||||
|
|
||||||
get _filenameNode() : HTMLInputElement { return this.shadowRoot.querySelector("#filename");}
|
get _filenameNode() : HTMLInputElement { return this.shadowRoot.querySelector("#filename");}
|
||||||
|
|
||||||
get _fileNodes() : HTMLElement[] { return Array.from(this.shadowRoot.querySelectorAll(".vfs_select__file"));}
|
get _fileNodes() : Et2VfsSelectRow[] { return Array.from(this.shadowRoot.querySelectorAll("et2-vfs-select-row"));}
|
||||||
|
|
||||||
get _searchNode() : HTMLInputElement { return this.shadowRoot.querySelector("#search");}
|
get _searchNode() : HTMLInputElement { return this.shadowRoot.querySelector("#search");}
|
||||||
|
|
||||||
@ -191,7 +197,7 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
|
|
||||||
if(this.path == "")
|
if(this.path == "")
|
||||||
{
|
{
|
||||||
this.path = "~";
|
this.path = <string>this.egw()?.preference("startfolder", "filemanager") || "~";
|
||||||
}
|
}
|
||||||
// Get file list
|
// Get file list
|
||||||
this.startSearch();
|
this.startSearch();
|
||||||
@ -207,6 +213,27 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties : PropertyValues)
|
||||||
|
{
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
debugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties : PropertyValues)
|
||||||
|
{
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
|
if(changedProperties.has("mode"))
|
||||||
|
{
|
||||||
|
this.multiple = this.mode == "open-multiple";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(changedProperties.has("path"))
|
||||||
|
{
|
||||||
|
this.startSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public setPath(path)
|
public setPath(path)
|
||||||
{
|
{
|
||||||
if(path == '..')
|
if(path == '..')
|
||||||
@ -244,6 +271,7 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
this.startSearch();
|
this.startSearch();
|
||||||
}
|
}
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
this.updateComplete,
|
||||||
this._searchPromise,
|
this._searchPromise,
|
||||||
this._dialog.show()
|
this._dialog.show()
|
||||||
]);
|
]);
|
||||||
@ -258,6 +286,14 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
return this._dialog.hide();
|
return this._dialog.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getComplete() : Promise<string[]>
|
||||||
|
{
|
||||||
|
return this._dialog.getComplete().then(() =>
|
||||||
|
{
|
||||||
|
return this.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
startSearch() : Promise<void>
|
startSearch() : Promise<void>
|
||||||
{
|
{
|
||||||
// Stop timeout timer
|
// Stop timeout timer
|
||||||
@ -285,8 +321,8 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
{
|
{
|
||||||
// Include a limit, even if options don't, to avoid massive lists breaking the UI
|
// Include a limit, even if options don't, to avoid massive lists breaking the UI
|
||||||
let sendOptions = {
|
let sendOptions = {
|
||||||
path: this._pathNode?.value ?? this.path,
|
path: this.path,
|
||||||
mime: this._mimeNode?.value ?? this.mime,
|
mime: this.mime,
|
||||||
num_rows: 100,
|
num_rows: 100,
|
||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
@ -298,10 +334,7 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
|
|
||||||
processRemoteResults(results) : FileInfo[]
|
processRemoteResults(results) : FileInfo[]
|
||||||
{
|
{
|
||||||
if(results.message)
|
this.helpText = results.message ?? "";
|
||||||
{
|
|
||||||
this.helpText = results.message;
|
|
||||||
}
|
|
||||||
this._fileList = results.files ?? [];
|
this._fileList = results.files ?? [];
|
||||||
|
|
||||||
return this._fileList;
|
return this._fileList;
|
||||||
@ -338,22 +371,178 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected files
|
||||||
|
* @param {Et2VfsSelectRow | Et2VfsSelectRow[]} file
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private setSelectedFiles(file : Et2VfsSelectRow | Et2VfsSelectRow[])
|
||||||
|
{
|
||||||
|
const newSelectedOptions = Array.isArray(file) ? file : [file];
|
||||||
|
|
||||||
|
// Clear existing selection
|
||||||
|
this._fileNodes.forEach(el =>
|
||||||
|
{
|
||||||
|
el.selected = false;
|
||||||
|
el.requestUpdate("selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the new selection
|
||||||
|
if(newSelectedOptions.length)
|
||||||
|
{
|
||||||
|
newSelectedOptions.forEach(el =>
|
||||||
|
{
|
||||||
|
el.selected = true;
|
||||||
|
el.requestUpdate("selected");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selection, value, and display label
|
||||||
|
this.selectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles a file's selected state
|
||||||
|
*/
|
||||||
|
private toggleFileSelection(file : Et2VfsSelectRow, force? : boolean)
|
||||||
|
{
|
||||||
|
if(force === true || force === false)
|
||||||
|
{
|
||||||
|
file.selected = force;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
file.selected = !file.selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.requestUpdate("selected");
|
||||||
|
this.selectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method must be called whenever the selection changes. It will update the selected file cache, the current
|
||||||
|
* value, and the display value
|
||||||
|
*/
|
||||||
|
private selectionChanged()
|
||||||
|
{
|
||||||
|
// Update selected files cache
|
||||||
|
this.selectedFiles = this._fileNodes.filter(el => el.selected);
|
||||||
|
|
||||||
|
// Update the value
|
||||||
|
if(this.multiple)
|
||||||
|
{
|
||||||
|
this.value = this.selectedFiles.map(el => el.value.path);
|
||||||
|
|
||||||
|
// TODO - show how many are selected?
|
||||||
|
/*
|
||||||
|
if(this.value.length === 0)
|
||||||
|
{
|
||||||
|
// When no items are selected, keep the value empty so the placeholder shows
|
||||||
|
this.displayLabel = '';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.displayLabel = this.localize.term('numOptionsSelected', this.selectedFiles.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.value = [this.selectedFiles[0]?.value.path] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected handleButtonClick(event : MouseEvent)
|
protected handleButtonClick(event : MouseEvent)
|
||||||
{
|
{
|
||||||
if(event.target.id !== "cancel")
|
|
||||||
{
|
|
||||||
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.requestUpdate("open", true);
|
this.requestUpdate("open", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleCreateDirectory(event : MouseEvent | KeyboardEvent)
|
/**
|
||||||
|
* Create a new directory in the current one
|
||||||
|
* @param {MouseEvent | KeyboardEvent} event
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async handleCreateDirectory(event : MouseEvent | KeyboardEvent)
|
||||||
{
|
{
|
||||||
throw new Error("Method not implemented.");
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFileClick(event : MouseEvent)
|
||||||
|
{
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const file : Et2VfsSelectRow = target.closest('et2-vfs-select-row');
|
||||||
|
const oldValue = this.value;
|
||||||
|
|
||||||
|
if(file && !file.disabled)
|
||||||
|
{
|
||||||
|
// Can't select a directory normally
|
||||||
|
if(file.value.isDir && this.mode != "select-dir")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.multiple)
|
||||||
|
{
|
||||||
|
this.toggleFileSelection(file);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.setSelectedFiles(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set focus after updating so the value is announced by screen readers
|
||||||
|
//this.updateComplete.then(() => this.displayInput.focus({ preventScroll: true }));
|
||||||
|
|
||||||
|
if(this.value !== oldValue)
|
||||||
|
{
|
||||||
|
// Emit after updating
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
this.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileDoubleClick(event : MouseEvent)
|
||||||
|
{
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const file : Et2VfsSelectRow = target.closest('et2-vfs-select-row');
|
||||||
|
|
||||||
|
if(file.value.isDir)
|
||||||
|
{
|
||||||
|
this.toggleFileSelection(file, false);
|
||||||
|
const oldPath = this.path;
|
||||||
|
this.setPath(file.value.path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Not a dir, just select it
|
||||||
|
this.handleFileClick(event);
|
||||||
|
|
||||||
|
// If we only want one, we've got it. Close.
|
||||||
|
if(!this.multiple)
|
||||||
|
{
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
handleSearchKeyDown(event)
|
handleSearchKeyDown(event)
|
||||||
{
|
{
|
||||||
clearTimeout(this._searchTimeout);
|
clearTimeout(this._searchTimeout);
|
||||||
@ -362,7 +551,7 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
if(['ArrowDown', 'ArrowUp'].includes(event.key) && this._fileList.length)
|
if(['ArrowDown', 'ArrowUp'].includes(event.key) && this._fileList.length)
|
||||||
{
|
{
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.setCurrentOption(this._fileNodes[0]);
|
this.setCurrentFile(this._fileNodes[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Start search immediately
|
// Start search immediately
|
||||||
@ -374,8 +563,10 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
}
|
}
|
||||||
else if(event.key == "Escape")
|
else if(event.key == "Escape")
|
||||||
{
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
this.value = [];
|
this.value = [];
|
||||||
this.close();
|
this.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +599,11 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
image="filemanager/fav_filter" noSubmit="true"
|
image="filemanager/fav_filter" noSubmit="true"
|
||||||
@click=${() => this.setPath("/apps/favorites")}
|
@click=${() => this.setPath("/apps/favorites")}
|
||||||
></et2-button>
|
></et2-button>
|
||||||
<et2-select-app id="app" emptyLabel="Applications" noLang="1"></et2-select-app>
|
<et2-select id="app" emptyLabel="Applications" noLang="1"
|
||||||
|
.select_options=${this.appList}
|
||||||
|
@change=${(e) => this.setPath("/apps/" + e.target.value)}
|
||||||
|
>
|
||||||
|
</et2-select>
|
||||||
<et2-button statustext="Create directory" id="createdir" class="createDir"
|
<et2-button statustext="Create directory" id="createdir" class="createDir"
|
||||||
arial-label=${this.egw().lang("Create directory")}
|
arial-label=${this.egw().lang("Create directory")}
|
||||||
noSubmit="true"
|
noSubmit="true"
|
||||||
@ -417,9 +612,10 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
@click=${this.handleCreateDirectory}
|
@click=${this.handleCreateDirectory}
|
||||||
></et2-button>
|
></et2-button>
|
||||||
<file id="upload_file" statustext="upload file" progress_dropdownlist="true" multiple="true"
|
<file id="upload_file" statustext="upload file" progress_dropdownlist="true" multiple="true"
|
||||||
onFinish="app.vfsSelectUI.storeFile"/>
|
onFinish="app.vfsSelectUI.storeFile"></file>
|
||||||
<et2-searchbox id="search"
|
<et2-searchbox id="search"
|
||||||
@keydown=${this.handleSearchKeyDown}
|
@keydown=${this.handleSearchKeyDown}
|
||||||
|
@sl-clear=${this.startSearch}
|
||||||
></et2-searchbox>
|
></et2-searchbox>
|
||||||
</et2-box>
|
</et2-box>
|
||||||
`;
|
`;
|
||||||
@ -428,28 +624,38 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
protected filesTemplate()
|
protected filesTemplate()
|
||||||
{
|
{
|
||||||
const empty = this._fileList.length == 0;
|
const empty = this._fileList.length == 0;
|
||||||
const noFilesTemplate = html`
|
|
||||||
<div class="vfs_select__empty">
|
|
||||||
<et2-image src="filemanager"></et2-image>
|
|
||||||
${this.egw().lang("no files in this directory.")}
|
|
||||||
</div>`;
|
|
||||||
const promise = this._searchPromise.then(() =>
|
const promise = this._searchPromise.then(() =>
|
||||||
{
|
{
|
||||||
return html`
|
return html`
|
||||||
${empty ? noFilesTemplate : html`
|
${empty ? this.noFilesTemplate() : html`
|
||||||
${repeat(this._fileList, (file) => file.path, (file, index) =>
|
${repeat(this._fileList, (file) => file.path, (file, index) =>
|
||||||
{
|
{
|
||||||
return html`
|
return html`
|
||||||
<et2-vfs-mime
|
<et2-vfs-select-row
|
||||||
.value=${file}
|
?disabled=${file.disabled || this.mode == "select-dir" && !file.isDir}
|
||||||
></et2-vfs-mime>
|
.value=${file}
|
||||||
${file.name}`;
|
@mouseup=${this.handleFileClick}
|
||||||
|
@dblclick=${this.handleFileDoubleClick}
|
||||||
|
></et2-vfs-select-row>`;
|
||||||
}
|
}
|
||||||
)}`
|
)}`
|
||||||
}`;
|
}`;
|
||||||
});
|
});
|
||||||
return html`
|
return html`
|
||||||
${until(promise, html`<sl-spinner></sl-spinner>`)}`;
|
${until(promise, html`
|
||||||
|
<div class="vfs_select__loading">
|
||||||
|
<sl-spinner></sl-spinner>
|
||||||
|
</div>`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected noFilesTemplate() : TemplateResult
|
||||||
|
{
|
||||||
|
return html`
|
||||||
|
<div class="vfs_select__empty">
|
||||||
|
<et2-image src="filemanager"></et2-image>
|
||||||
|
${this.egw().lang("no files in this directory.")}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected mimeOptionsTemplate()
|
protected mimeOptionsTemplate()
|
||||||
@ -502,8 +708,8 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<et2-dialog
|
<et2-dialog
|
||||||
.isModal="true"
|
.isModal=${true}
|
||||||
.destroyOnClose="false"
|
.destroyOnClose=${false}
|
||||||
.title=${this.title}
|
.title=${this.title}
|
||||||
.open=${this.open}
|
.open=${this.open}
|
||||||
>
|
>
|
||||||
@ -562,7 +768,6 @@ export class Et2VfsSelect extends Et2InputWidget(LitElement) implements SearchMi
|
|||||||
</et2-dialog>
|
</et2-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("et2-vfs-select", Et2VfsSelect);
|
customElements.define("et2-vfs-select", Et2VfsSelect);
|
||||||
@ -573,4 +778,6 @@ export interface FileInfo
|
|||||||
mime : string,
|
mime : string,
|
||||||
isDir : boolean,
|
isDir : boolean,
|
||||||
path? : string,
|
path? : string,
|
||||||
|
// We want to show it, but not act with it. File is disabled for the UI
|
||||||
|
disabled? : boolean
|
||||||
}
|
}
|
96
api/js/etemplate/Et2Vfs/Et2VfsSelectRow.styles.ts
Normal file
96
api/js/etemplate/Et2Vfs/Et2VfsSelectRow.styles.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import {css} from 'lit';
|
||||||
|
|
||||||
|
export default css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:focus) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([disabled]) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--sl-font-sans);
|
||||||
|
font-size: var(--sl-font-size-medium);
|
||||||
|
font-weight: var(--sl-font-weight-normal);
|
||||||
|
line-height: var(--sl-line-height-normal);
|
||||||
|
letter-spacing: var(--sl-letter-spacing-normal);
|
||||||
|
color: var(--sl-color-neutral-700);
|
||||||
|
padding: var(--sl-spacing-2x-small) var(--sl-spacing-medium) var(--sl-spacing-2x-small) var(--sl-spacing-x-small);
|
||||||
|
transition: var(--sl-transition-fast) fill;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file--hover:not(.file--current):not(.file--disabled) {
|
||||||
|
background-color: var(--sl-color-neutral-100);
|
||||||
|
color: var(--sl-color-neutral-1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file--current,
|
||||||
|
.file--current.file--disabled {
|
||||||
|
background-color: var(--sl-color-primary-600);
|
||||||
|
color: var(--sl-color-neutral-0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file--disabled {
|
||||||
|
outline: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file__label {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: var(--sl-line-height-dense);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file .file__check {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
visibility: hidden;
|
||||||
|
padding-inline-end: var(--sl-spacing-2x-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file et2-vfs-mime {
|
||||||
|
/* line-height-normal has no unit */
|
||||||
|
height: calc(var(--sl-line-height-normal) * 1em);
|
||||||
|
padding-inline-end: var(--sl-spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file--selected .file__check {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file__prefix,
|
||||||
|
.file__suffix {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file__prefix::slotted(*) {
|
||||||
|
margin-inline-end: var(--sl-spacing-x-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file__suffix::slotted(*) {
|
||||||
|
margin-inline-start: var(--sl-spacing-x-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
:host(:hover:not([aria-disabled='true'])) .file {
|
||||||
|
outline: dashed 1px SelectedItem;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
75
api/js/etemplate/Et2Vfs/Et2VfsSelectRow.ts
Normal file
75
api/js/etemplate/Et2Vfs/Et2VfsSelectRow.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import {html, LitElement} from "lit";
|
||||||
|
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||||
|
import {FileInfo} from "./Et2VfsSelect";
|
||||||
|
import {property} from "lit/decorators/property.js";
|
||||||
|
import {state} from "lit/decorators/state.js";
|
||||||
|
import {classMap} from "lit/directives/class-map.js";
|
||||||
|
import shoelace from "../Styles/shoelace";
|
||||||
|
import styles from "./Et2VfsSelectRow.styles";
|
||||||
|
|
||||||
|
export class Et2VfsSelectRow extends Et2Widget(LitElement)
|
||||||
|
{
|
||||||
|
static get styles()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
shoelace,
|
||||||
|
...super.styles,
|
||||||
|
styles
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({type: Object}) value : FileInfo;
|
||||||
|
|
||||||
|
/** Draws the file in a disabled state, preventing selection. */
|
||||||
|
@property({type: Boolean, reflect: true}) disabled = false;
|
||||||
|
|
||||||
|
@state() current = false; // the user has keyed into the file, but hasn't selected it yet (shows a highlight)
|
||||||
|
@state() selected = false; // the file is selected and has aria-selected="true"
|
||||||
|
@state() hasHover = false; // we need this because Safari doesn't honor :hover styles while dragging
|
||||||
|
|
||||||
|
connectedCallback()
|
||||||
|
{
|
||||||
|
super.connectedCallback();
|
||||||
|
this.setAttribute('role', 'option');
|
||||||
|
this.setAttribute('aria-selected', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseEnter()
|
||||||
|
{
|
||||||
|
this.hasHover = true;
|
||||||
|
this.requestUpdate("hasHover", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseLeave()
|
||||||
|
{
|
||||||
|
this.hasHover = false;
|
||||||
|
this.requestUpdate("hasHover", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
part="base"
|
||||||
|
class=${classMap({
|
||||||
|
file: true,
|
||||||
|
'file--current': this.current,
|
||||||
|
'file--disabled': this.disabled,
|
||||||
|
'file--selected': this.selected,
|
||||||
|
'file--hover': this.hasHover
|
||||||
|
})}
|
||||||
|
@mouseenter=${this.handleMouseEnter}
|
||||||
|
@mouseleave=${this.handleMouseLeave}
|
||||||
|
>
|
||||||
|
<sl-icon part="checked-icon" class="file__check" name="check" library="system"
|
||||||
|
aria-hidden="true"></sl-icon>
|
||||||
|
<slot part="prefix" name="prefix" class="file__prefix"></slot>
|
||||||
|
<et2-vfs-mime .value=${this.value}></et2-vfs-mime>
|
||||||
|
${this.value.name}
|
||||||
|
<slot part="suffix" name="suffix" class="file__suffix"></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("et2-vfs-select-row", Et2VfsSelectRow);
|
@ -98,6 +98,7 @@ import "./Layout/Et2Split/Et2Split";
|
|||||||
import "./Layout/RowLimitedMixin";
|
import "./Layout/RowLimitedMixin";
|
||||||
import "./Et2Vfs/Et2VfsMime";
|
import "./Et2Vfs/Et2VfsMime";
|
||||||
import "./Et2Vfs/Et2VfsSelect";
|
import "./Et2Vfs/Et2VfsSelect";
|
||||||
|
import "./Et2Vfs/Et2VfsSelectRow";
|
||||||
import "./Et2Vfs/Et2VfsUid";
|
import "./Et2Vfs/Et2VfsUid";
|
||||||
import "./Et2Textbox/Et2Password";
|
import "./Et2Textbox/Et2Password";
|
||||||
import './Et2Textbox/Et2Searchbox';
|
import './Et2Textbox/Et2Searchbox';
|
||||||
|
@ -612,49 +612,129 @@ class Vfs extends File
|
|||||||
*/
|
*/
|
||||||
public static function ajax_vfsSelectFiles($search, $content)
|
public static function ajax_vfsSelectFiles($search, $content)
|
||||||
{
|
{
|
||||||
$pathIn = $options['path'] ?? '~';
|
$response = [];
|
||||||
if($pathIn == '~')
|
$content['path'] = $content['path'] ?? '~';
|
||||||
|
if($content['path'] == '~')
|
||||||
{
|
{
|
||||||
$pathIn = Api\Vfs::get_home_dir();
|
$content['path'] = Api\Vfs::get_home_dir();
|
||||||
}
|
}
|
||||||
$content = [];
|
|
||||||
if(!($files = Api\Vfs::find($pathIn, array(
|
// Filemanager favorites as directories
|
||||||
|
if(substr($content['path'], 0, strlen('/apps/favorites')) == '/apps/favorites')
|
||||||
|
{
|
||||||
|
$files = static::filesFromFavorites($search, $content);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$files = static::filesFromVfs($search, $content);
|
||||||
|
if(is_string($files))
|
||||||
|
{
|
||||||
|
$response['message'] = $files;
|
||||||
|
$files = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach($files as $path)
|
||||||
|
{
|
||||||
|
if(is_string($path) && $path == $content['path'] || is_array($path) && $path['path'] == $content['path'])
|
||||||
|
{
|
||||||
|
// remove directory itself
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$name = $path['name'] ?? Api\Vfs::basename($path);
|
||||||
|
$is_dir = $path['isDir'] ?? Api\Vfs::is_dir($path);
|
||||||
|
$mime = $path['mime'] ?? Api\Vfs::mime_content_type($path);
|
||||||
|
if($content['mime'] && !$is_dir && $mime != $content['mime'])
|
||||||
|
{
|
||||||
|
continue; // does not match mime-filter --> ignore
|
||||||
|
}
|
||||||
|
$response['files'][] = array(
|
||||||
|
'name' => $name,
|
||||||
|
'path' => $path,
|
||||||
|
'mime' => $mime,
|
||||||
|
'isDir' => $is_dir
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Json\Response::get()->data($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function filesFromVfs($search, $params)
|
||||||
|
{
|
||||||
|
$vfs_options = array(
|
||||||
'dirsontop' => true,
|
'dirsontop' => true,
|
||||||
'order' => 'name',
|
'order' => 'name',
|
||||||
'sort' => 'ASC',
|
'sort' => 'ASC',
|
||||||
'maxdepth' => 1,
|
'maxdepth' => 1,
|
||||||
))))
|
);
|
||||||
|
if($search)
|
||||||
{
|
{
|
||||||
$content['message'] = lang("Can't open directory %1!", $pathIn);
|
$vfs_options['name_preg'] = '/' . str_replace(array('\\?', '\\*'),
|
||||||
|
array('.{1}', '.*'),
|
||||||
|
preg_quote($search)) . '/i';
|
||||||
}
|
}
|
||||||
else
|
if($params['num_rows'])
|
||||||
{
|
{
|
||||||
$n = 0;
|
$vfs_options['limit'] = (int)$params['num_rows'];
|
||||||
foreach($files as $path)
|
}
|
||||||
{
|
if(!($files = Api\Vfs::find($params['path'], $vfs_options)))
|
||||||
if($path == $pathIn)
|
{
|
||||||
{
|
return lang("Can't open directory %1!", $params['path']);
|
||||||
continue;
|
}
|
||||||
} // remove directory itself
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
$name = Api\Vfs::basename($path);
|
/**
|
||||||
$is_dir = Api\Vfs::is_dir($path);
|
* Get favorites as if they were folders
|
||||||
$mime = Api\Vfs::mime_content_type($path);
|
*
|
||||||
if($content['mime'] && !$is_dir && $mime != $content['mime'])
|
* @return array
|
||||||
{
|
*/
|
||||||
continue; // does not match mime-filter --> ignore
|
private static function filesFromFavorites($search, $params)
|
||||||
}
|
{
|
||||||
$content['files'][$n] = array(
|
|
||||||
'name' => $name,
|
// Display favorites as if they were folders
|
||||||
'path' => $path,
|
$files = array();
|
||||||
|
$favorites = Api\Framework\Favorites::get_favorites('filemanager');
|
||||||
|
|
||||||
|
//check for recent paths and add them to the top of favorites list
|
||||||
|
if(is_array($params['recentPaths']))
|
||||||
|
{
|
||||||
|
foreach($params['recentPaths'] as $p)
|
||||||
|
{
|
||||||
|
$mime = Api\Vfs::mime_content_type($p);
|
||||||
|
$files[] = array(
|
||||||
|
'name' => $p,
|
||||||
|
'path' => $p,
|
||||||
'mime' => $mime,
|
'mime' => $mime,
|
||||||
'is_dir' => $is_dir
|
'is_dir' => true
|
||||||
);
|
);
|
||||||
++$n;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$response = Json\Response::get();
|
|
||||||
$response->data($content);
|
foreach($favorites as $favorite)
|
||||||
|
{
|
||||||
|
$path = $favorite['state']['path'];
|
||||||
|
if(!$path)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Search
|
||||||
|
if($search && !(str_contains($favorite['name'], $search) || str_contains($path, $search)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!Api\Vfs::is_readable($path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mime = Api\Vfs::mime_content_type($path);
|
||||||
|
$files[] = array(
|
||||||
|
'name' => $favorite['name'],
|
||||||
|
'path' => $path,
|
||||||
|
'mime' => $mime,
|
||||||
|
'isDir' => true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $files;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user