Framework WIP:

- Starting to get app loading
This commit is contained in:
nathan 2024-05-08 08:32:23 -06:00
parent e3d66c2cc6
commit a9d57499a3
4 changed files with 203 additions and 68 deletions

View File

@ -61,30 +61,6 @@
{hook_after_navbar} {hook_after_navbar}
<!-- END framework -->
<!--
<div id="egw_fw_basecontainer" lang="{lang_code}">
<div id="egw_fw_header">
<div id="egw_fw_topmenu">
<div id="egw_fw_topmenu_items">
{topmenu_items}
<div class="timezone">
{user_info}
</div>
{powered_by}
</div>
</div>
</div>
<div id="egw_fw_sidebar">
<div id="egw_fw_sidemenu"></div>
<div id="egw_fw_splitter"></div>
</div>
<div id="egw_fw_main">
<div id="egw_fw_tabs">
</div>
</div>
</div>
<div id="egw_fw_firstload"> <div id="egw_fw_firstload">
{firstload_animation} {firstload_animation}
</div> </div>

View File

@ -2,10 +2,12 @@ import {css, html, LitElement} from "lit";
import {customElement} from "lit/decorators/custom-element.js"; import {customElement} from "lit/decorators/custom-element.js";
import {property} from "lit/decorators/property.js"; import {property} from "lit/decorators/property.js";
import {classMap} from "lit/directives/class-map.js"; import {classMap} from "lit/directives/class-map.js";
import {repeat} from "lit/directives/repeat.js";
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js"; import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
import styles from "./EgwFramework.styles"; import styles from "./EgwFramework.styles";
import {repeat} from "lit/directives/repeat.js"; import {egw} from "../../api/js/jsapi/egw_global";
import {Function} from "estree"; import {SlTab, SlTabGroup} from "@shoelace-style/shoelace";
import {EgwFrameworkApp} from "./EgwFrameworkApp";
/** /**
* @summary Accessable, webComponent-based EGroupware framework * @summary Accessable, webComponent-based EGroupware framework
@ -94,9 +96,9 @@ export class EgwFramework extends LitElement
@property({type: Array, attribute: "application-list"}) @property({type: Array, attribute: "application-list"})
applicationList = []; applicationList = [];
get egw() get egw() : typeof egw
{ {
return window.egw ?? { return window.egw ?? <typeof egw>{
// Dummy egw so we don't get failures from missing methods // Dummy egw so we don't get failures from missing methods
lang: (t) => t, lang: (t) => t,
preference: (n, app, promise? : Function | boolean | undefined) => Promise.resolve(""), preference: (n, app, promise? : Function | boolean | undefined) => Promise.resolve(""),
@ -104,15 +106,103 @@ export class EgwFramework extends LitElement
}; };
} }
/**
*
* @param _function Framework function to be called on the server.
* @param _ajax_exec_url Actual function we want to call.
* @returns {string}
*/
public getMenuaction(_fun, _ajax_exec_url, appName = 'home')
{
let baseUrl = this.getBaseUrl();
// Check whether the baseurl is actually set. If not, then this application
// resides inside the same egw instance as the jdots framework. We'll simply
// return a menu action and not a full featured url here.
if(baseUrl != '')
{
baseUrl = baseUrl + 'json.php?menuaction=';
}
const menuaction = _ajax_exec_url ? _ajax_exec_url.match(/menuaction=([^&]+)/) : null;
// use template handler to call current framework, eg. pixelegg
return baseUrl + appName + '.kdots_framework.' + _fun + '.template' +
(menuaction ? '.' + menuaction[1] : '');
};
public loadApp(appname)
{
const app = this.applicationList.find(a => a.name == appname);
let appComponent = <EgwFrameworkApp>document.createElement("egw-app");
appComponent.id = appname;
appComponent.name = appname;
appComponent.url = app?.url;
this.append(appComponent);
app.opened = this.shadowRoot.querySelectorAll("sl-tab").length;
this.requestUpdate("applicationList");
return appComponent;
}
protected getBaseUrl() {return "";}
/** /**
* An application tab is chosen, show the app * An application tab is chosen, show the app
* *
* @param e * @param e
* @protected * @protected
*/ */
protected handleApplicationTabShow(e) protected handleApplicationTabShow(event)
{ {
this.querySelectorAll("egw-app").forEach(app => app.removeAttribute("active"));
// Create & show app
const appname = event.target.activeTab.panel;
let appComponent = this.querySelector(`egw-app#${appname}`);
if(!appComponent)
{
appComponent = this.loadApp(appname);
}
appComponent.setAttribute("active", "");
// Update the list on the server
this.updateTabs(event.target.activeTab);
}
/**
* An application tab is closed
*/
protected handleApplicationTabClose(event)
{
const tabGroup : SlTabGroup = this.shadowRoot.querySelector("sl-tab-group.egw_fw__open_applications");
const tab = event.target;
const panel = tabGroup.querySelector(`sl-tab-panel[name="${tab.panel}"]`);
// Show the previous tab if the tab is currently active
if(tab.active)
{
tabGroup.show(tab.previousElementSibling.panel);
}
else
{
// Show will update, but closing in the background we call directly
this.updateTabs(tabGroup.querySelector("sl-tab[active]"));
}
// Remove the tab + panel
tab.remove();
panel.remove();
}
private updateTabs(activeTab)
{
let appList = [];
Array.from(this.shadowRoot.querySelectorAll("sl-tab-group.egw_fw__open_applications sl-tab")).forEach((tab : SlTab) =>
{
appList.push({appName: tab.panel, active: activeTab.panel == tab.panel})
});
this.egw.jsonq('EGroupware\\Api\\Framework\\Ajax::ajax_tab_changed_state', [appList]);
} }
/** /**
@ -134,7 +224,7 @@ export class EgwFramework extends LitElement
protected _applicationTabTemplate(app) protected _applicationTabTemplate(app)
{ {
return html` return html`
<sl-tab slot="nav" panel="${app.app}" closable aria-label="${app.title}"> <sl-tab slot="nav" panel="${app.name}" closable aria-label="${app.title}">
<sl-tooltip placement="bottom" content="${app.title}" hoist> <sl-tooltip placement="bottom" content="${app.title}" hoist>
<et2-image src="${app.icon}"></et2-image> <et2-image src="${app.icon}"></et2-image>
</sl-tooltip> </sl-tooltip>
@ -167,8 +257,10 @@ export class EgwFramework extends LitElement
</sl-dropdown> </sl-dropdown>
<sl-tab-group part="open-applications" class="egw_fw__open_applications" activation="manual" <sl-tab-group part="open-applications" class="egw_fw__open_applications" activation="manual"
role="tablist" role="tablist"
@sl-tab-show=${this.handleApplicationTabShow}> @sl-tab-show=${this.handleApplicationTabShow}
${repeat(this.applicationList.filter(app => app.opened), (app) => this._applicationTabTemplate(app))} @sl-close=${this.handleApplicationTabClose}
>
${repeat(this.applicationList.filter(app => app.opened).sort((a, b) => a.opened - b.opened), (app) => this._applicationTabTemplate(app))}
</sl-tab-group> </sl-tab-group>
<slot name="header"><span class="placeholder">header</span></slot> <slot name="header"><span class="placeholder">header</span></slot>
<slot name="header-right"><span class="placeholder">header-right</span></slot> <slot name="header-right"><span class="placeholder">header-right</span></slot>

View File

@ -88,6 +88,9 @@ export class EgwFrameworkApp extends LitElement
@property() @property()
name = "Application name"; name = "Application name";
@property()
url = "";
@state() @state()
leftCollapsed = false; leftCollapsed = false;
@ -122,6 +125,8 @@ export class EgwFrameworkApp extends LitElement
}; };
private resizeTimeout : number; private resizeTimeout : number;
protected loadingPromise = Promise.resolve();
connectedCallback() connectedCallback()
{ {
super.connectedCallback(); super.connectedCallback();
@ -133,6 +138,64 @@ export class EgwFrameworkApp extends LitElement
{ {
this.rightPanelInfo.preferenceWidth = parseInt(width) ?? this.rightPanelInfo.defaultWidth; this.rightPanelInfo.preferenceWidth = parseInt(width) ?? this.rightPanelInfo.defaultWidth;
}); });
// Register the "data" plugin
this.egw.registerJSONPlugin(this.jsonDataHandler, this, 'data');
}
disconnectedCallback()
{
super.disconnectedCallback();
this.egw.unregisterJSONPlugin(this.jsonDataHandler, this, "data", false)
}
firstUpdated()
{
this.load(this.url);
}
protected load(url)
{
if(!url)
{
while(this.firstChild)
{
this.removeChild(this.lastChild);
}
return;
}
let targetUrl = "";
let useIframe = false;
let matches = url.match(/\/index.php\?menuaction=([A-Za-z0-9_\.]*.*&ajax=true.*)$/);
if(matches)
{
// Matches[1] contains the menuaction which should be executed - replace
// the given url with the following line. This will be evaluated by the
// jdots_framework ajax_exec function which will be called by the code
// below as we set useIframe to false.
targetUrl = "index.php?menuaction=" + matches[1];
useIframe = false;
}
// Destroy application js
if(window.app[this.name] && window.app[this.name].destroy)
{
window.app[this.name].destroy();
delete window.app[this.name]; // really delete it, so new object get constructed and registered for push
}
return this.loadingPromise = this.egw.request(
this.framework.getMenuaction('ajax_exec', targetUrl, this.name),
[targetUrl]
);
}
protected jsonDataHandler(type, res, req)
{
if(req.context !== this)
{
return;
}
debugger;
} }
public showLeft() public showLeft()
@ -174,6 +237,11 @@ export class EgwFrameworkApp extends LitElement
return window.egw ?? (<EgwFramework>this.parentElement).egw ?? null; return window.egw ?? (<EgwFramework>this.parentElement).egw ?? null;
} }
get framework() : EgwFramework
{
return this.closest("egw-framework");
}
/** /**
* User adjusted side slider, update preference * User adjusted side slider, update preference
* *
@ -207,21 +275,33 @@ export class EgwFrameworkApp extends LitElement
} }
} }
protected _asideTemplate(parentSlot, side, label?)
{
const asideClassMap = classMap({
"egw_fw_app__aside": true,
"egw_fw_app__left": true,
"egw_fw_app__aside-collapsed": this.leftCollapsed,
});
return html`
<aside slot="${parentSlot}" part="${side}" class=${asideClassMap} aria-label="${label}">
<div class="egw_fw_app__aside_header header">
<slot name="${side}-header"><span class="placeholder">${side}-header</span></slot>
</div>
<div class="egw_fw_app__aside_content content">
<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>`;
}
render() render()
{ {
const hasLeftSlots = this.hasSlotController.test('left-header') || this.hasSlotController.test('left') || this.hasSlotController.test('left-footer'); 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 hasRightSlots = this.hasSlotController.test('right-header') || this.hasSlotController.test('right') || this.hasSlotController.test('right-footer');
const leftClassMap = classMap({
"egw_fw_app__aside": true,
"egw_fw_app__left": true,
"egw_fw_app__aside-collapsed": this.leftCollapsed,
});
const rightClassMap = classMap({
"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 : const leftWidth = this.leftCollapsed || !hasLeftSlots ? this.leftPanelInfo.hiddenWidth :
this.leftPanelInfo.preferenceWidth; this.leftPanelInfo.preferenceWidth;
const rightWidth = this.rightCollapsed || !hasRightSlots ? this.rightPanelInfo.hiddenWidth : const rightWidth = this.rightCollapsed || !hasRightSlots ? this.rightPanelInfo.hiddenWidth :
@ -268,18 +348,7 @@ export class EgwFrameworkApp extends LitElement
this.leftCollapsed = !this.leftCollapsed; this.leftCollapsed = !this.leftCollapsed;
this.requestUpdate(); this.requestUpdate();
}}></sl-icon> }}></sl-icon>
<aside slot="start" part="left" class=${leftClassMap}> ${this._asideTemplate("start", "left")}
<div class="egw_fw_app__aside_header header">
<slot name="left-header"><span class="placeholder">left-header</span></slot>
</div>
<div class="egw_fw_app__aside_content content">
<slot name="left"><span class="placeholder">left</span></slot>
</div>
<div class="egw_fw_app__aside_footer footer">
<slot name="left-footer"><span class="placeholder">left-footer</span></slot>
</div>
</aside>
<sl-split-panel slot="end" <sl-split-panel slot="end"
class=${classMap({"egw_fw_app__innerSplit": true, "no-content": !hasRightSlots})} class=${classMap({"egw_fw_app__innerSplit": true, "no-content": !hasRightSlots})}
primary="start" primary="start"
@ -303,18 +372,7 @@ export class EgwFrameworkApp extends LitElement
<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=${rightClassMap} part="right" ${this._asideTemplate("end", "right", this.egw.lang("%1 application details", this.egw.lang(this.name)))}
aria-label="${this.egw.lang("%1 application details", this.egw.lang(this.name))}">
<header class="egw_fw_app__aside_header header">
<slot name="right-header"><span class="placeholder">right-header</span></slot>
</header>
<div class="egw_fw_app__aside_content content" tabindex="0">
<slot name="right"><span class="placeholder">right</span></slot>
</div>
<footer class="egw_fw_app__aside_footer footer">
<slot name="right-footer"><span class="placeholder">right-footer</span></slot>
</footer>
</aside>
</sl-split-panel> </sl-split-panel>
</sl-split-panel> </sl-split-panel>
</div> </div>

View File

@ -2,12 +2,21 @@
* app.ts is auto-built * app.ts is auto-built
*/ */
import "./EgwFramework"; import {EgwFramework} from "./EgwFramework";
import "./EgwFrameworkApp"; import {EgwFrameworkApp} from "./EgwFrameworkApp";
document.addEventListener('DOMContentLoaded', () => document.addEventListener('DOMContentLoaded', () =>
{ {
// Not sure what's up here
if(!window.customElements.get("egw-framework"))
{
window.customElements.define("egw-framework", EgwFramework);
}
if(!window.customElements.get("egw-app"))
{
window.customElements.define("egw-app", EgwFrameworkApp);
}
/* Set up listener on avatar menu */ /* Set up listener on avatar menu */
const avatarMenu = document.querySelector("#topmenu_info_user_avatar"); const avatarMenu = document.querySelector("#topmenu_info_user_avatar");
avatarMenu.addEventListener("sl-select", (e : CustomEvent) => avatarMenu.addEventListener("sl-select", (e : CustomEvent) =>