diff --git a/api/js/etemplate/Et2Vfs/Et2VfsPath.styles.ts b/api/js/etemplate/Et2Vfs/Et2VfsPath.styles.ts index 467208f060..566112678d 100644 --- a/api/js/etemplate/Et2Vfs/Et2VfsPath.styles.ts +++ b/api/js/etemplate/Et2Vfs/Et2VfsPath.styles.ts @@ -6,7 +6,7 @@ export default css` flex: 1; display: flex; flex-direction: row; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; justify-content: space-between; gap: 0.1rem 0.5rem; @@ -28,8 +28,7 @@ export default css` .vfs-path__value-input { flex: 1 1 auto; - order: 10; - min-width: 7em; + min-width: 20em; border: none; outline: none; color: var(--input-text-color); @@ -44,6 +43,7 @@ export default css` .vfs-path__edit { flex-grow: 0; display: inline-flex; + visibility: hidden; align-items: center; justify-content: center; font-size: inherit; @@ -53,11 +53,34 @@ export default css` padding: 0; transition: var(--sl-transition-fast) color; cursor: pointer; - margin-left: auto; + } + + :host(:hover) .vfs-path__edit { + visibility: visible; } /* Breadcrumb directories */ + sl-breadcrumb { + flex: 1 1 auto; + overflow: hidden; + min-width: 20em; + } + + et2-image { + flex: none; + height: 1.5em; + } + + sl-breadcrumb::part(base) { + flex-wrap: nowrap; + flex-direction: row-reverse; + justify-content: flex-start; + } + + sl-breadcrumb-item::part(base) { + font-size: var(--sl-font-size-medium); + } sl-breadcrumb-item::part(label) { color: var(--input-text-color); } @@ -69,6 +92,18 @@ export default css` cursor: pointer; } + sl-breadcrumb-item:first-of-type { + margin-right: auto; + } + + sl-breadcrumb-item:first-of-type::part(separator) { + display: none; + } + + sl-breadcrumb-item:last-of-type::part(separator) { + display: initial; + } + /* Sizes */ .form-control--medium, .form-control--medium .form-control-input { diff --git a/api/js/etemplate/Et2Vfs/Et2VfsPath.ts b/api/js/etemplate/Et2Vfs/Et2VfsPath.ts index e77b652ef7..e0f26bca67 100644 --- a/api/js/etemplate/Et2Vfs/Et2VfsPath.ts +++ b/api/js/etemplate/Et2Vfs/Et2VfsPath.ts @@ -8,7 +8,7 @@ */ import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget"; -import {html, LitElement, nothing} from "lit"; +import {html, LitElement, nothing, TemplateResult} from "lit"; import shoelace from "../Styles/shoelace"; import styles from "./Et2VfsPath.styles"; import {property} from "lit/decorators/property.js"; @@ -18,6 +18,7 @@ import {repeat} from "lit/directives/repeat.js"; import {FileInfo} from "./Et2VfsSelectDialog"; import {SlBreadcrumbItem} from "@shoelace-style/shoelace"; import {HasSlotController} from "../Et2Widget/slot"; +import {until} from "lit/directives/until.js"; /** * @summary Display an editable path from the VFS @@ -144,6 +145,8 @@ export class Et2VfsPath extends Et2InputWidget(LitElement) protected handleEditMouseDown(event : MouseEvent) { + event.preventDefault(); + event.stopPropagation(); this.edit(); } @@ -162,8 +165,6 @@ export class Et2VfsPath extends Et2InputWidget(LitElement) event.preventDefault(); this.blur(); break; - - } } @@ -174,16 +175,21 @@ export class Et2VfsPath extends Et2InputWidget(LitElement) { target = target.parentNode; } + else if(target instanceof Image && target.parentElement.slot == "prefix") + { + // Icon + target = target.parentElement.parentElement; + } if(target instanceof SlBreadcrumbItem && event.composedPath().includes(this)) { event.preventDefault(); event.stopPropagation(); - const dirs = Array.from(target.parentElement.querySelectorAll('sl-breadcrumb-item')) ?? []; + const dirs = Array.from(target.parentElement.querySelectorAll('sl-breadcrumb-item')).reverse() ?? []; let stopIndex = dirs.indexOf(target) + 1; let newPath = dirs.slice(0, stopIndex) // Strip out any extra space - .map(d => d.textContent.trim().replace(/\/*$/, '').trim() + "/") + .map(d => (d.dataset.value ?? "").trim().replace(/\/*$/, '').trim() + "/") .filter(p => p); if(newPath[0] !== '/') { @@ -220,6 +226,45 @@ export class Et2VfsPath extends Et2InputWidget(LitElement) } } + protected _getIcon(pathParts) + { + let image = this.egw().image("filemanager", "api"); + if(pathParts.length > 2 && pathParts[1] == "apps") + { + image = this.egw().image('navbar', pathParts[2].toLowerCase()); + } + + return image; + } + + protected pathPartTemplate(pathParts, path, i) + { + let index = pathParts.length - 1 - i; + let pathName : string | TemplateResult<1> = path.trim(); + if(pathParts.length > 1 && pathParts[1] == "apps") + { + switch(index) + { + case 1: + pathName = this.egw().lang("applications"); + break; + case 2: + pathName = this.egw().lang(pathName); + break; + case 3: + if(!isNaN(pathName)) + { + pathName = html`${until(this.egw().link_title(pathParts[2], pathParts[3], true) || pathName, pathName)}` + } + } + } + return html` + + ${pathName} + / + `; + } + render() { const hasLabelSlot = this.hasSlotController.test('label'); @@ -227,13 +272,15 @@ export class Et2VfsPath extends Et2InputWidget(LitElement) const hasLabel = this.label ? true : !!hasLabelSlot; const hasHelpText = this.helpText ? true : !!hasHelpTextSlot; // No trailing slash in the path - const pathParts = this.value === "/" ? ["/"] : this.value + const pathParts = this.value // Remove trailing / .replace(/\/*$/, '') .split('/'); const isEditable = !(this.disabled || this.readonly); const editing = this.editing && isEditable; + let icon = this._getIcon(pathParts); + return html`
this.focus()} > - + + ${icon ? html` + + { + this.setValue("/"); + this.updateComplete.then(() => {this.dispatchEvent(new Event("change", {bubbles: true}))}); + }} + >` : nothing} + ${editing ? html` this.blur()} @keydown=${this.handleKeyDown} - />` : html` + /> +
` : html` / - ${repeat(pathParts, (path) => - { - return html` - ${path.trim()} - / - `; - })} + ${repeat(pathParts.toReversed(), (part, i) => this.pathPartTemplate(pathParts, part, i))} ${!isEditable ? nothing : html`