mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-07 08:34:29 +01:00
Framework WIP:
- App areas resizable with styles & saving to preference - App sides fully hidden when no content
This commit is contained in:
parent
6cee24a5c2
commit
50e67e5d46
@ -51,6 +51,8 @@
|
|||||||
<!-- Fake app -->
|
<!-- Fake app -->
|
||||||
<egw-app name="fake app" class="placeholder">
|
<egw-app name="fake app" class="placeholder">
|
||||||
<div style="border: 1px dotted">Something inside the app - main</div>
|
<div style="border: 1px dotted">Something inside the app - main</div>
|
||||||
|
<div slot="left">Left content</div>
|
||||||
|
<div slot="right">right content</div>
|
||||||
</egw-app>
|
</egw-app>
|
||||||
</egw-framework>
|
</egw-framework>
|
||||||
|
|
||||||
|
@ -35,16 +35,42 @@ export default css`
|
|||||||
grid-column: start / end;
|
grid-column: start / end;
|
||||||
grid-row: start / end;
|
grid-row: start / end;
|
||||||
grid-template-rows: subgrid;
|
grid-template-rows: subgrid;
|
||||||
|
--min: var(--left-min, 0px);
|
||||||
|
--max: var(--left-max, 20%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.egw_fw_app__innerSplit {
|
.egw_fw_app__innerSplit {
|
||||||
grid-template-rows: subgrid;
|
grid-template-rows: subgrid;
|
||||||
grid-column-end: -1;
|
grid-column-end: -1;
|
||||||
grid-row: start / end;
|
grid-row: start / end;
|
||||||
|
--max: calc(100% - var(--right-min, 0px));
|
||||||
|
--min: calc(100% - var(--right-max, 50%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.egw_fw_app__innerSplit.no-content {
|
||||||
|
--min: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
sl-split-panel::part(divider) {
|
sl-split-panel::part(divider) {
|
||||||
grid-row: start / end;
|
grid-row: start / end;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl-split-panel > sl-icon {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: var(--sl-border-radius-small);
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
color: var(--sl-color-neutral-0);
|
||||||
|
padding: 0.5rem 0.125rem;
|
||||||
|
z-index: var(--sl-z-index-drawer);
|
||||||
|
}
|
||||||
|
|
||||||
|
sl-split-panel.no-content {
|
||||||
|
--divider-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl-split-panel.no-content::part(divider) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.egw_fw_app__aside {
|
.egw_fw_app__aside {
|
||||||
|
@ -5,11 +5,13 @@ import {state} from "lit/decorators/state.js";
|
|||||||
import {classMap} from "lit/directives/class-map.js";
|
import {classMap} from "lit/directives/class-map.js";
|
||||||
|
|
||||||
import styles from "./EgwFrameworkApp.styles";
|
import styles from "./EgwFrameworkApp.styles";
|
||||||
|
import {SlSplitPanel} from "@shoelace-style/shoelace";
|
||||||
|
import {HasSlotController} from "../../api/js/etemplate/Et2Widget/slot";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Application component inside EgwFramework
|
* @summary Application component inside EgwFramework
|
||||||
*
|
*
|
||||||
* @dependency sl-icon-button
|
* @dependency sl-split-panel
|
||||||
*
|
*
|
||||||
* @slot - Main application content. Other slots are normally hidden if they have no content
|
* @slot - Main application content. Other slots are normally hidden if they have no content
|
||||||
* @slot header - Top of app, contains logo, app icons.
|
* @slot header - Top of app, contains logo, app icons.
|
||||||
@ -29,7 +31,10 @@ import styles from "./EgwFrameworkApp.styles";
|
|||||||
* @csspart right - Right optional content.
|
* @csspart right - Right optional content.
|
||||||
* @csspart footer - Very bottom of the main content.
|
* @csspart footer - Very bottom of the main content.
|
||||||
*
|
*
|
||||||
* @cssproperty [--icon-size=32] - Height of icons used in the framework
|
* @cssproperty [--left-min=0] - Minimum width of the left content
|
||||||
|
* @cssproperty [--left-max=20%] - Maximum width of the left content
|
||||||
|
* @cssproperty [--right-min=0] - Minimum width of the right content
|
||||||
|
* @cssproperty [--right-max=50%] - Maximum width of the right content
|
||||||
*/
|
*/
|
||||||
@customElement('egw-app')
|
@customElement('egw-app')
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -82,27 +87,151 @@ export class EgwFrameworkApp extends LitElement
|
|||||||
@state()
|
@state()
|
||||||
leftCollapsed = false;
|
leftCollapsed = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
rightCollapsed = false;
|
||||||
|
|
||||||
|
get leftSplitter() { return <SlSplitPanel>this.shadowRoot.querySelector(".egw_fw_app__outerSplit");}
|
||||||
|
|
||||||
|
get rightSplitter() { return <SlSplitPanel>this.shadowRoot.querySelector(".egw_fw_app__innerSplit");}
|
||||||
|
|
||||||
|
|
||||||
|
protected readonly hasSlotController = new HasSlotController(<LitElement><unknown>this,
|
||||||
|
'left', 'left-header', 'left-footer',
|
||||||
|
'right', 'right-header', 'right-footer',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Left is in pixels
|
||||||
|
private leftPanelInfo : PanelInfo = {
|
||||||
|
side: "left",
|
||||||
|
preference: "jdotssideboxwidth",
|
||||||
|
defaultWidth: 200,
|
||||||
|
hiddenWidth: 0,
|
||||||
|
preferenceWidth: 200
|
||||||
|
};
|
||||||
|
// Right is in percentage
|
||||||
|
private rightPanelInfo : PanelInfo = {
|
||||||
|
side: "right",
|
||||||
|
preference: "app_right_width",
|
||||||
|
defaultWidth: 50,
|
||||||
|
hiddenWidth: 100,
|
||||||
|
preferenceWidth: 50
|
||||||
|
};
|
||||||
|
private resizeTimeout : number;
|
||||||
|
|
||||||
|
connectedCallback()
|
||||||
|
{
|
||||||
|
super.connectedCallback();
|
||||||
|
this.egw.preference(this.leftPanelInfo.preference, this.name, true).then((width) =>
|
||||||
|
{
|
||||||
|
this.leftPanelInfo.preferenceWidth = parseInt(width ?? this.leftPanelInfo.defaultWidth);
|
||||||
|
});
|
||||||
|
this.egw.preference(this.rightPanelInfo.preference, this.name, true).then((width) =>
|
||||||
|
{
|
||||||
|
this.rightPanelInfo.preferenceWidth = parseInt(width ?? this.rightPanelInfo.defaultWidth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public showLeft()
|
||||||
|
{
|
||||||
|
this.showSide("left");
|
||||||
|
}
|
||||||
|
|
||||||
|
public hideLeft()
|
||||||
|
{
|
||||||
|
this.hideSide("left");
|
||||||
|
}
|
||||||
|
|
||||||
|
public showRight()
|
||||||
|
{
|
||||||
|
this.showSide("right");
|
||||||
|
}
|
||||||
|
|
||||||
|
public hideRight()
|
||||||
|
{
|
||||||
|
this.hideSide("right");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected showSide(side)
|
||||||
|
{
|
||||||
|
const attribute = `${side}Collapsed`;
|
||||||
|
this[attribute] = false;
|
||||||
|
this[`${side}Splitter`].position = this[`${side}PanelInfo`].preferenceWidth || this[`${side}PanelInfo`].defaultWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hideSide(side : "left" | "right")
|
||||||
|
{
|
||||||
|
const attribute = `${side}Collapsed`;
|
||||||
|
this[attribute] = true;
|
||||||
|
this[`${side}Splitter`].position = this[`${side}PanelInfo`].hiddenWidth;
|
||||||
|
}
|
||||||
|
|
||||||
get egw()
|
get egw()
|
||||||
{
|
{
|
||||||
return window.egw ?? this.parentElement.egw ?? null;
|
return window.egw ?? this.parentElement.egw ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User adjusted side slider, update preference
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async handleSlide(event)
|
||||||
|
{
|
||||||
|
if(typeof event.target?.panelInfo != "object")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Left side is in pixels, round to 2 decimals
|
||||||
|
let newPosition = Math.round(event.target.panelInfo.side == "left" ? event.target.positionInPixels * 100 : event.target.position * 100) / 100;
|
||||||
|
let preferenceName = event.target.panelInfo.preference;
|
||||||
|
if(newPosition != event.target.panelInfo.preferenceWidth)
|
||||||
|
{
|
||||||
|
event.target.panelInfo.preferenceWidth = newPosition;
|
||||||
|
if(this.resizeTimeout)
|
||||||
|
{
|
||||||
|
window.clearTimeout(this.resizeTimeout);
|
||||||
|
}
|
||||||
|
window.setTimeout(() =>
|
||||||
|
{
|
||||||
|
this.egw.set_preference(this.name, preferenceName, newPosition);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
|
const hasLeftSlots = this.hasSlotController.test('left-header') || this.hasSlotController.test('left') || this.hasSlotController.test('left-footer');
|
||||||
|
const hasRightSlots = this.hasSlotController.test('right-header') || this.hasSlotController.test('right') || this.hasSlotController.test('right-footer');
|
||||||
|
|
||||||
const leftClassMap = classMap({
|
const leftClassMap = classMap({
|
||||||
"egw_fw_app__aside": true,
|
"egw_fw_app__aside": true,
|
||||||
"egw_fw_app__left": true,
|
"egw_fw_app__left": true,
|
||||||
"egw_fw_app__aside-collapsed": this.leftCollapsed
|
"egw_fw_app__aside-collapsed": this.leftCollapsed,
|
||||||
});
|
});
|
||||||
const leftWidth = this.leftCollapsed ? 0 : parseInt(this.egw.preference("jdotssideboxwidth", this.name) || 0) || 250;
|
const rightClassMap = classMap({
|
||||||
const rightWidth = (parseInt(this.egw.preference("app_right_width", this.name) || 0) || 0);
|
"egw_fw_app__aside": true,
|
||||||
|
"egw_fw_app__right": true,
|
||||||
|
"egw_fw_app__aside-collapsed": this.rightCollapsed,
|
||||||
|
});
|
||||||
|
const leftWidth = this.leftCollapsed || !hasLeftSlots ? this.leftPanelInfo.hiddenWidth :
|
||||||
|
this.leftPanelInfo.preferenceWidth;
|
||||||
|
const rightWidth = this.rightCollapsed || !hasRightSlots ? this.rightPanelInfo.hiddenWidth :
|
||||||
|
this.rightPanelInfo.preferenceWidth;
|
||||||
return html`
|
return html`
|
||||||
<div class="egw_fw_app__header">
|
<div class="egw_fw_app__header">
|
||||||
<div class="egw_fw_app__name" part="name">
|
<div class="egw_fw_app__name" part="name">
|
||||||
|
${hasLeftSlots ? html`
|
||||||
<sl-icon-button name="${this.leftCollapsed ? "chevron-double-right" : "chevron-double-left"}"
|
<sl-icon-button name="${this.leftCollapsed ? "chevron-double-right" : "chevron-double-left"}"
|
||||||
label="${this.egw?.lang("Hide area")}"
|
label="${this.egw?.lang("Hide area")}"
|
||||||
@click=${() => {this.leftCollapsed = !this.leftCollapsed}}
|
@click=${() =>
|
||||||
></sl-icon-button>
|
{
|
||||||
|
this.leftCollapsed = !this.leftCollapsed;
|
||||||
|
this.requestUpdate("leftCollapsed")
|
||||||
|
}}
|
||||||
|
></sl-icon-button>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
<h2>${this.egw?.lang(this.name) ?? this.name}</h2>
|
<h2>${this.egw?.lang(this.name) ?? this.name}</h2>
|
||||||
</div>
|
</div>
|
||||||
<header class="egw_fw_app__header" part="header">
|
<header class="egw_fw_app__header" part="header">
|
||||||
@ -110,7 +239,17 @@ export class EgwFrameworkApp extends LitElement
|
|||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<main class="egw_fw_app__main" part="main" aria-label="${this.name}" tabindex="0">
|
<main class="egw_fw_app__main" part="main" aria-label="${this.name}" tabindex="0">
|
||||||
<sl-split-panel class="egw_fw_app__outerSplit" primary="start" position-in-pixels="${leftWidth}">
|
<sl-split-panel class=${classMap({"egw_fw_app__outerSplit": true, "no-content": !hasLeftSlots})}
|
||||||
|
primary="start" position-in-pixels="${leftWidth}"
|
||||||
|
snap="0px 20%" snap-threshold="50"
|
||||||
|
.panelInfo=${this.leftPanelInfo}
|
||||||
|
@sl-reposition=${(e) => this.handleSlide(e)}
|
||||||
|
>
|
||||||
|
<sl-icon slot="divider" name="grip-vertical" @dblclick=${() =>
|
||||||
|
{
|
||||||
|
this.leftCollapsed = !this.leftCollapsed;
|
||||||
|
this.requestUpdate();
|
||||||
|
}}></sl-icon>
|
||||||
<aside slot="start" part="left" class=${leftClassMap}>
|
<aside slot="start" part="left" class=${leftClassMap}>
|
||||||
<div class="egw_fw_app__aside_header header">
|
<div class="egw_fw_app__aside_header header">
|
||||||
<slot name="left-header"><span class="placeholder">left-header</span></slot>
|
<slot name="left-header"><span class="placeholder">left-header</span></slot>
|
||||||
@ -123,20 +262,29 @@ export class EgwFrameworkApp extends LitElement
|
|||||||
<slot name="left-footer"><span class="placeholder">left-footer</span></slot>
|
<slot name="left-footer"><span class="placeholder">left-footer</span></slot>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<sl-split-panel slot="end" class="egw_fw_app__innerSplit" primary="start"
|
<sl-split-panel slot="end"
|
||||||
position-in-pixels=${rightWidth || nothing}
|
class=${classMap({"egw_fw_app__innerSplit": true, "no-content": !hasRightSlots})}
|
||||||
position=${rightWidth == 0 ? "100" : nothing}
|
primary="start"
|
||||||
|
position=${rightWidth} snap="50% 80% 100%"
|
||||||
|
snap-threshold="50"
|
||||||
|
.panelInfo=${this.rightPanelInfo}
|
||||||
|
@sl-reposition=${(e) => this.handleSlide(e)}
|
||||||
>
|
>
|
||||||
|
<sl-icon slot="divider" name="grip-vertical" @dblclick=${() =>
|
||||||
|
{
|
||||||
|
this.rightCollapsed = !this.rightCollapsed;
|
||||||
|
this.requestUpdate();
|
||||||
|
}}></sl-icon>
|
||||||
<header slot="start" class="egw_fw_app__header header" part="content-header">
|
<header slot="start" class="egw_fw_app__header header" part="content-header">
|
||||||
<slot name="header"><span class="placeholder">header</span></slot>
|
<slot name="header"><span class="placeholder">header</span></slot>
|
||||||
</header>
|
</header>
|
||||||
<main slot=start" class="egw_fw_app__main_content content" part="content">
|
<main slot="start" class="egw_fw_app__main_content content" part="content">
|
||||||
<slot><span class="placeholder">main</span></slot>
|
<slot><span class="placeholder">main</span></slot>
|
||||||
</main>
|
</main>
|
||||||
<footer slot="start" class="egw_fw_app__footer footer" part="footer">
|
<footer slot="start" class="egw_fw_app__footer footer" part="footer">
|
||||||
<slot name="footer"><span class="placeholder">main-footer</span></slot>
|
<slot name="footer"><span class="placeholder">main-footer</span></slot>
|
||||||
</footer>
|
</footer>
|
||||||
<aside slot="end" class="egw_fw_app__aside egw_fw_app__right" part="right">
|
<aside slot="end" class=${rightClassMap} part="right">
|
||||||
<div class="egw_fw_app__aside_header header">
|
<div class="egw_fw_app__aside_header header">
|
||||||
<slot name="right-header"><span class="placeholder">right-header</span></slot>
|
<slot name="right-header"><span class="placeholder">right-header</span></slot>
|
||||||
</div>
|
</div>
|
||||||
@ -152,4 +300,12 @@ export class EgwFrameworkApp extends LitElement
|
|||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PanelInfo = {
|
||||||
|
side : "left" | "right",
|
||||||
|
preference : "jdotssideboxwidth" | "app_right_width",
|
||||||
|
hiddenWidth : number,
|
||||||
|
defaultWidth : number,
|
||||||
|
preferenceWidth : number | string
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user