mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-02-22 13:20:50 +01:00
Merge remote-tracking branch 'origin/master' into upstream_master
This commit is contained in:
commit
530a28e13a
176
api/js/egw_action/EgwMenuShoelace.ts
Normal file
176
api/js/egw_action/EgwMenuShoelace.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import {css, html, LitElement, nothing} from "lit";
|
||||
import {SlMenu, SlMenuItem} from "@shoelace-style/shoelace";
|
||||
import {egwMenuItem} from "./egw_menu";
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
|
||||
@customElement("egw-menu-shoelace")
|
||||
export class EgwMenuShoelace extends LitElement
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
/* Fit in popup, scroll if not enough height */
|
||||
max-height: var(--auto-size-available-height, auto);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.default-item::part(label) {
|
||||
font-weight: var(--sl-font-weight-bold, bold);
|
||||
}
|
||||
|
||||
et2-image {
|
||||
width: 1.5em;
|
||||
}
|
||||
`
|
||||
]
|
||||
}
|
||||
|
||||
private structure = [];
|
||||
private popup = null;
|
||||
private removeCallback = null;
|
||||
|
||||
private get menu() : SlMenu { return this.shadowRoot?.querySelector("sl-menu");}
|
||||
|
||||
constructor(_structure : egwMenuItem[])
|
||||
{
|
||||
super();
|
||||
this.structure = _structure;
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
disconnectedCallback()
|
||||
{
|
||||
super.disconnectedCallback();
|
||||
if(this.popup)
|
||||
{
|
||||
this.popup.remove();
|
||||
this.popup = null;
|
||||
}
|
||||
if(this.removeCallback)
|
||||
{
|
||||
this.removeCallback.call();
|
||||
}
|
||||
}
|
||||
|
||||
public showAt(_x, _y, _onHide)
|
||||
{
|
||||
this.removeCallback = _onHide;
|
||||
if(this.popup == null)
|
||||
{
|
||||
this.popup = document.createElement("sl-popup");
|
||||
this.popup.placement = "bottom";
|
||||
this.popup.autoSize = "vertical";
|
||||
this.popup.flip = true;
|
||||
this.popup.shift = true;
|
||||
this.popup.classList.add("egw_menu")
|
||||
document.body.append(this.popup);
|
||||
this.popup.append(this);
|
||||
}
|
||||
this.popup.anchor = {
|
||||
getBoundingClientRect()
|
||||
{
|
||||
return {
|
||||
x: _x,
|
||||
y: _y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: _y,
|
||||
left: _x,
|
||||
right: _x,
|
||||
bottom: _y
|
||||
}
|
||||
}
|
||||
};
|
||||
this.popup.active = true;
|
||||
Promise.all([this.updateComplete, this.popup.updateComplete]).then(() =>
|
||||
{
|
||||
// Causes scroll issues if we don't position
|
||||
this.popup.popup.style = "top: 0px";
|
||||
(<SlMenuItem>this.menu.querySelector('sl-menu-item')).focus();
|
||||
});
|
||||
}
|
||||
|
||||
public hide()
|
||||
{
|
||||
this.popup.active = false;
|
||||
}
|
||||
|
||||
handleSelect(event)
|
||||
{
|
||||
if(!this.popup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(event.detail.item.value)
|
||||
{
|
||||
const item = <egwMenuItem>event.detail.item.value;
|
||||
if(item.checkbox || typeof item.checked !== "undefined")
|
||||
{
|
||||
item.checked = event.detail.item.checked;
|
||||
return;
|
||||
}
|
||||
if(typeof item.onClick == "function")
|
||||
{
|
||||
this.hide();
|
||||
item.onClick.call(event.detail.item, item, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private itemTemplate(item : egwMenuItem)
|
||||
{
|
||||
if(item.caption == "-")
|
||||
{
|
||||
return html`
|
||||
<sl-divider></sl-divider>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sl-menu-item
|
||||
class=${classMap({
|
||||
"default-item": item.default
|
||||
})}
|
||||
id=${item.id}
|
||||
type="${item.checkbox ? "checkbox" : "normal"}"
|
||||
?checked=${item.checkbox && item.checked}
|
||||
?disabled=${!item.enabled}
|
||||
.value=${item}
|
||||
>
|
||||
${item.iconUrl ? html`
|
||||
<et2-image slot="prefix" src="${item.iconUrl}"></et2-image>` : nothing}
|
||||
${item.caption}
|
||||
${item.shortcutCaption ? html`<span slot="suffix"
|
||||
class="keyboard_shortcut">
|
||||
${item.shortcutCaption}
|
||||
</span>` : nothing}
|
||||
${item.children.length == 0 ? nothing : html`
|
||||
<sl-menu slot="submenu">
|
||||
${repeat(item.children, i => this.itemTemplate(i))}
|
||||
</sl-menu>
|
||||
`}
|
||||
</sl-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
render()
|
||||
{
|
||||
return html`
|
||||
<sl-menu
|
||||
@sl-select=${this.handleSelect}
|
||||
>
|
||||
${repeat(this.structure, i => this.itemTemplate(i))}
|
||||
</sl-menu>`;
|
||||
}
|
||||
}
|
@ -9,14 +9,18 @@
|
||||
*
|
||||
*/
|
||||
import {egwMenuImpl} from './egw_menu_dhtmlx';
|
||||
import {EgwMenuShoelace} from "./EgwMenuShoelace";
|
||||
import {egw_registeredShortcuts, egw_shortcutIdx} from './egw_keymanager';
|
||||
import {
|
||||
EGW_KEY_ARROW_DOWN,
|
||||
EGW_KEY_ARROW_LEFT,
|
||||
EGW_KEY_ARROW_RIGHT,
|
||||
EGW_KEY_ARROW_UP, EGW_KEY_ENTER,
|
||||
EGW_KEY_ARROW_UP,
|
||||
EGW_KEY_ENTER,
|
||||
EGW_KEY_ESCAPE
|
||||
} from "./egw_action_constants";
|
||||
import {EgwFramework} from "../../../kdots/js/EgwFramework";
|
||||
|
||||
//Global variable which is used to store the currently active menu so that it
|
||||
//may be closed when another menu opens
|
||||
export var _egw_active_menu: egwMenu = null;
|
||||
@ -209,7 +213,14 @@ export class egwMenu
|
||||
if (this.instance == null && this._checkImpl)
|
||||
{
|
||||
//Obtain a new egwMenuImpl object and pass this instance to it
|
||||
this.instance = new egwMenuImpl(this.children);
|
||||
if(window.framework instanceof EgwFramework)
|
||||
{
|
||||
this.instance = new EgwMenuShoelace(this.children);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.instance = new egwMenuImpl(this.children);
|
||||
}
|
||||
|
||||
_egw_active_menu = this;
|
||||
|
||||
@ -238,6 +249,12 @@ export class egwMenu
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shoelace does its own keyboard navigation
|
||||
if(!this.instance.dhtmlxmenu)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//TODO change with shoelace
|
||||
let current = this.instance.dhtmlxmenu.menuSelected;
|
||||
if (current !== -1)
|
||||
|
@ -345,20 +345,21 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
// Hide the trash
|
||||
trash.remove();
|
||||
|
||||
// Delete preference server side
|
||||
Favorite.remove(this.egw(), this.app, line.value).then(response =>
|
||||
// Delete preference server side, returns boolean
|
||||
Favorite.remove(this.egw(), this.app, line.value).then(result =>
|
||||
{
|
||||
line.classList.remove("loading");
|
||||
|
||||
let result = response.response.find(r => r.type == "data");
|
||||
this.dispatchEvent(new CustomEvent("preferenceChange", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
application: this.application,
|
||||
preference: line.value
|
||||
}
|
||||
}));
|
||||
|
||||
// Could not find the result we want
|
||||
if(!result || result.type !== "data")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof result.data == 'boolean' && result.data)
|
||||
if(result)
|
||||
{
|
||||
// Remove line from list
|
||||
line.remove();
|
||||
|
@ -46,6 +46,9 @@ export class Et2FavoritesMenu extends Et2Widget(LitElement)
|
||||
@property()
|
||||
application : string;
|
||||
|
||||
@property()
|
||||
noAdd : boolean = false;
|
||||
|
||||
private favorites : { [name : string] : Favorite } = {
|
||||
'blank': {
|
||||
name: typeof this.egw()?.lang == "function" ? this.egw().lang("No filters") : "No filters",
|
||||
@ -55,24 +58,63 @@ export class Et2FavoritesMenu extends Et2Widget(LitElement)
|
||||
};
|
||||
private loadingPromise = Promise.resolve();
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.handlePreferenceChange = this.handlePreferenceChange.bind(this);
|
||||
}
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
if(this.application)
|
||||
{
|
||||
this.loadingPromise = Favorite.load(this.egw(), this.application).then((favorites) =>
|
||||
{
|
||||
this.favorites = favorites;
|
||||
});
|
||||
this._load();
|
||||
}
|
||||
document.addEventListener("preferenceChange", this.handlePreferenceChange);
|
||||
}
|
||||
|
||||
disconnectedCallback()
|
||||
{
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener("preferenceChange", this.handlePreferenceChange);
|
||||
}
|
||||
|
||||
private _load()
|
||||
{
|
||||
this.loadingPromise = Favorite.load(this.egw(), this.application).then((favorites) =>
|
||||
{
|
||||
this.favorites = favorites;
|
||||
});
|
||||
}
|
||||
|
||||
handlePreferenceChange(e)
|
||||
{
|
||||
if(e && e.detail?.application == this.application)
|
||||
{
|
||||
this._load();
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
handleSelect(event)
|
||||
{
|
||||
if(event.detail.item.value == Favorite.ADD_VALUE)
|
||||
{
|
||||
return this.handleAdd(event);
|
||||
}
|
||||
Favorite.applyFavorite(this.egw(), this.application, event.detail.item.value);
|
||||
}
|
||||
|
||||
handleAdd(event)
|
||||
{
|
||||
event.stopPropagation();
|
||||
if(this.egw().window && this.egw().window.app[this.application])
|
||||
{
|
||||
this.egw().window.app[this.application].add_favorite({});
|
||||
}
|
||||
}
|
||||
|
||||
handleDelete(event)
|
||||
{
|
||||
// Don't trigger click
|
||||
@ -89,6 +131,18 @@ export class Et2FavoritesMenu extends Et2Widget(LitElement)
|
||||
// Remove from widget
|
||||
delete this.favorites[favoriteName];
|
||||
this.requestUpdate();
|
||||
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
this.dispatchEvent(new CustomEvent("preferenceChange", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
application: this.application,
|
||||
preference: favoriteName
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
this.requestUpdate();
|
||||
@ -123,12 +177,21 @@ export class Et2FavoritesMenu extends Et2Widget(LitElement)
|
||||
{
|
||||
return html`
|
||||
<sl-menu
|
||||
part="menu"
|
||||
@sl-select=${this.handleSelect}
|
||||
>
|
||||
${this.label ? html`
|
||||
<sl-menu-label>${this.label}</sl-menu-label>` : nothing}
|
||||
${repeat(Object.keys(this.favorites), (i) => this.menuItemTemplate(i, this.favorites[i]))}
|
||||
<slot></slot>
|
||||
${this.noAdd ? nothing : html`
|
||||
<sl-menu-item value=${Favorite.ADD_VALUE}
|
||||
@sl-select=${this.handleAdd}
|
||||
>
|
||||
<sl-icon name="plus" slot="prefix"></sl-icon>
|
||||
${this.egw().lang("Current view as favourite")}
|
||||
</sl-menu-item>`
|
||||
}
|
||||
</sl-menu>
|
||||
`;
|
||||
});
|
||||
|
@ -1246,6 +1246,15 @@ export abstract class EgwApp
|
||||
this.egw.set_preference(this.appname, favorite_pref, favorite);
|
||||
}
|
||||
|
||||
// Trigger event so widgets can update
|
||||
document.dispatchEvent(new CustomEvent("preferenceChange", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
application: this.appname,
|
||||
preference: favorite_pref
|
||||
}
|
||||
}));
|
||||
|
||||
// Add to list immediately
|
||||
if(this.sidebox)
|
||||
{
|
||||
|
@ -114,7 +114,7 @@ class calendar_timezones
|
||||
}
|
||||
}
|
||||
// check for a Windows timezone without "Standard Time" postfix
|
||||
if (!isset($id) && strpos($tzid, '/') === false)
|
||||
if (!isset($id) && stripos($tzid, ' Standard Time') === false)
|
||||
{
|
||||
$id = self::tz2id($tzid.' Standard Time');
|
||||
}
|
||||
|
@ -147,4 +147,26 @@ class kdots_framework extends Api\Framework\Ajax
|
||||
$title .
|
||||
'</sl-menu-item>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set site-wide CSS like preferred font-size
|
||||
*
|
||||
* @return array
|
||||
* @see Api\Framework::_get_css()
|
||||
*/
|
||||
public function _get_css()
|
||||
{
|
||||
$ret = parent::_get_css();
|
||||
|
||||
$textsize = $GLOBALS['egw_info']['user']['preferences']['common']['textsize'] ?? '12';
|
||||
$ret['app_css'] .= "
|
||||
:root, :host, body, input {
|
||||
font-size: {$textsize}px;
|
||||
font-family: egroupware, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
";
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +148,7 @@ export default css`
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.egw_fw_app__main_content {
|
||||
@ -221,4 +222,13 @@ export default css`
|
||||
padding: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
sl-details.favorites::part(content) {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
sl-details.favorites et2-favorites-menu::part(menu) {
|
||||
border: none;
|
||||
|
||||
}
|
||||
|
||||
`
|
@ -13,6 +13,7 @@ import {etemplate2} from "../../api/js/etemplate/etemplate2";
|
||||
import {et2_IPrint} from "../../api/js/etemplate/et2_core_interfaces";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {until} from "lit/directives/until.js";
|
||||
import {Favorite} from "../../api/js/etemplate/Et2Favorites/Favorite";
|
||||
|
||||
/**
|
||||
* @summary Application component inside EgwFramework
|
||||
@ -139,6 +140,7 @@ export class EgwFrameworkApp extends LitElement
|
||||
/** The application's content must be in an iframe instead of handled normally */
|
||||
protected useIframe = false;
|
||||
protected _sideboxData : any;
|
||||
private _hasFavorites = false;
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
@ -261,6 +263,17 @@ export class EgwFrameworkApp extends LitElement
|
||||
public setSidebox(sideboxData, hash?)
|
||||
{
|
||||
this._sideboxData = sideboxData;
|
||||
|
||||
if(this._sideboxData?.some(s => s.title == "Favorites" || s.title == this.egw.lang("favorites")))
|
||||
{
|
||||
// This might be a little late, but close enough for rendering
|
||||
Favorite.load(this.egw, this.name).then((favorites) =>
|
||||
{
|
||||
this._hasFavorites = (Object.values(favorites).length > 1)
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@ -429,27 +442,27 @@ export class EgwFrameworkApp extends LitElement
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if there's no side-content
|
||||
if(!this.hasSideContent(event.target.panelInfo.side))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let panelInfo = event.target.panelInfo;
|
||||
|
||||
await this.loadingPromise;
|
||||
|
||||
// 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 newPosition = Math.round(panelInfo.side == "left" ? event.target.positionInPixels * 100 : Math.max(100, event.target.position) * 100) / 100;
|
||||
|
||||
// Update collapsed
|
||||
this[`${event.target.panelInfo.side}Collapsed`] = newPosition == event.target.panelInfo.hiddenWidth;
|
||||
this[`${panelInfo.side}Collapsed`] = newPosition == panelInfo.hiddenWidth;
|
||||
|
||||
let preferenceName = event.target.panelInfo.preference;
|
||||
if(newPosition != event.target.panelInfo.preferenceWidth && !isNaN(newPosition))
|
||||
let preferenceName = panelInfo.preference;
|
||||
let currentPreference = parseFloat("" + await this.egw.preference(preferenceName, this.name, true));
|
||||
|
||||
if(newPosition != currentPreference && !isNaN(newPosition))
|
||||
{
|
||||
event.target.panelInfo.preferenceWidth = newPosition;
|
||||
if(this.resizeTimeout)
|
||||
panelInfo.preferenceWidth = newPosition;
|
||||
if(panelInfo.resizeTimeout)
|
||||
{
|
||||
window.clearTimeout(this.resizeTimeout);
|
||||
window.clearTimeout(panelInfo.resizeTimeout);
|
||||
}
|
||||
this.resizeTimeout = window.setTimeout(() =>
|
||||
panelInfo.resizeTimeout = window.setTimeout(() =>
|
||||
{
|
||||
this.egw.set_preference(this.name, preferenceName, newPosition);
|
||||
|
||||
@ -500,12 +513,13 @@ export class EgwFrameworkApp extends LitElement
|
||||
<iframe src="${this.url}"></iframe>`;
|
||||
}
|
||||
|
||||
protected _asideTemplate(parentSlot, side, label?)
|
||||
protected _asideTemplate(parentSlot, side : "left" | "right", label?)
|
||||
{
|
||||
const asideClassMap = classMap({
|
||||
"egw_fw_app__aside": true,
|
||||
"egw_fw_app__left": true,
|
||||
"egw_fw_app__aside-collapsed": this.leftCollapsed,
|
||||
"egw_fw_app__left": side == "left",
|
||||
"egw_fw_app__right": side == "right",
|
||||
"egw_fw_app__aside-collapsed": side == "left" ? this.leftCollapsed : this.rightCollapsed,
|
||||
});
|
||||
return html`
|
||||
<aside slot="${parentSlot}" part="${side}" class=${asideClassMap} aria-label="${label}">
|
||||
@ -513,15 +527,47 @@ export class EgwFrameworkApp extends LitElement
|
||||
<slot name="${side}-header"><span class="placeholder">${side}-header</span></slot>
|
||||
</div>
|
||||
<div class="egw_fw_app__aside_content content">
|
||||
${side == "left" ? this._leftMenuTemplate() : nothing}
|
||||
<slot name="${side}"><span class="placeholder">${side}</span></slot>
|
||||
</div>
|
||||
|
||||
<div class="egw_fw_app__aside_footer footer">
|
||||
<slot name="${side}-footer"><span class="placeholder">${side}-footer</span></slot>
|
||||
</div>
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Left sidebox automatic content
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected _leftMenuTemplate()
|
||||
{
|
||||
// Put favorites in left sidebox if any are set
|
||||
if(!this._hasFavorites)
|
||||
{
|
||||
return nothing;
|
||||
}
|
||||
return html`${until(Favorite.load(this.egw, this.name).then((favorites) =>
|
||||
{
|
||||
// If more than the blank favorite is found, add favorite menu to sidebox
|
||||
if(Object.values(favorites).length > 1)
|
||||
{
|
||||
const favSidebox = this._sideboxData.find(s => s.title.toLowerCase() == "favorites" || s.title == this.egw.lang("favorites"));
|
||||
return html`
|
||||
<sl-details class="favorites" slot="left"
|
||||
?open=${favSidebox?.opened}
|
||||
summary=${this.egw.lang("Favorites")}
|
||||
@sl-show=${() => {this.egw.set_preference(this.name, 'jdots_sidebox_' + favSidebox.menu_name, true);}}
|
||||
@sl-hide=${() => {this.egw.set_preference(this.name, 'jdots_sidebox_' + favSidebox.menu_name, false);}}
|
||||
>
|
||||
<et2-favorites-menu application=${this.name}></et2-favorites-menu>
|
||||
</sl-details>
|
||||
`;
|
||||
}
|
||||
}), nothing)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top right header, contains application action buttons (reload, print, config)
|
||||
* @returns {TemplateResult<1>}
|
||||
@ -563,14 +609,21 @@ export class EgwFrameworkApp extends LitElement
|
||||
</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
` : nothing}
|
||||
${this._sideboxMenuTemplate()}
|
||||
${this._threeDotsMenuTemplate()}
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</sl-tooltip>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _sideboxMenuTemplate()
|
||||
/**
|
||||
* This is the "three dots menu" in the top-right corner.
|
||||
* Most of what was in the sidebox now goes here.
|
||||
*
|
||||
* @returns {TemplateResult<1> | typeof nothing }
|
||||
* @protected
|
||||
*/
|
||||
protected _threeDotsMenuTemplate()
|
||||
{
|
||||
if(!this._sideboxData)
|
||||
{
|
||||
@ -635,7 +688,7 @@ export class EgwFrameworkApp extends LitElement
|
||||
|
||||
render()
|
||||
{
|
||||
const hasLeftSlots = this.hasSideContent("left");
|
||||
const hasLeftSlots = this.hasSideContent("left") || this._hasFavorites;
|
||||
const hasRightSlots = this.hasSideContent("right");
|
||||
|
||||
const leftWidth = this.leftCollapsed || !hasLeftSlots ? this.leftPanelInfo.hiddenWidth :
|
||||
|
Loading…
Reference in New Issue
Block a user