Add new framework WIP

This commit is contained in:
nathan 2024-04-25 11:35:34 -06:00
parent b3609b3f4a
commit 0bcb402b2e
9 changed files with 664 additions and 0 deletions

View File

@ -113,6 +113,12 @@ abstract class Framework extends Framework\Extra
{
$GLOBALS['egw_info']['server']['template_set'] = 'pixelegg';
}
if($GLOBALS['egw_info']['user']['preferences']['common']['template_set'] !== $GLOBALS['egw_info']['server']['template_set'] &&
class_exists($class = $GLOBALS['egw_info']['user']['preferences']['common']['template_set'] . '_framework')
)
{
$GLOBALS['egw_info']['server']['template_set'] = $GLOBALS['egw_info']['user']['preferences']['common']['template_set'];
}
// then jdots aka Stylite template
if (file_exists(EGW_SERVER_ROOT.'/jdots') && empty($GLOBALS['egw_info']['server']['template_set']))
{

92
kdots/head.tpl Normal file
View File

@ -0,0 +1,92 @@
<!-- BEGIN head --><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xml:lang="{lang_code}" xmlns="http://www.w3.org/1999/xhtml"{dir_code} data-darkmode={darkmode}>
<head>
<title>{website_title}</title>
<meta http-equiv="content-type" content="text/html; charset={charset}"/>
<meta name="keywords" content="EGroupware"/>
<meta name="description" content="EGroupware"/>
<meta name="keywords" content="EGroupware"/>
<meta name="copyright" content="EGroupware GmbH https://www.egroupware.org (c) 2020"/>
<meta name="language" content="{lang_code}"/>
<meta name="author" content="EGroupware GmbH https://www.egroupware.org"/>
{meta_robots}
<link rel="manifest" href="{webserver_url}/manifest.json"/>
<link rel="icon" href="{img_icon}" type="image/x-ico"/>
<link rel="shortcut icon" href="{img_shortcut}"/>
<link rel="stylesheet" href="{webserver_url}/api/js/offline/themes/offline-theme-slide.css">
<link rel="stylesheet" href="{webserver_url}/api/js/offline/themes/offline-language-{lang_code}.css">
<script src="{webserver_url}/api/js/offline/offline.min.js"></script>
<link rel="stylesheet" href="{webserver_url}/node_modules/@shoelace-style/shoelace/dist/themes/light.css"/>
<link rel="stylesheet" href="{webserver_url}/node_modules/@shoelace-style/shoelace/dist/themes/dark.css"/>
<link rel="stylesheet" href="{webserver_url}/kdots/assets/styles/framework.css">
{css_file}
<style type="text/css">
{app_css}
</style>
<style type="text/css">
{firstload_animation_style}
</style>
<script type="module" src="{webserver_url}/kdots/js/app.min.js"></script>
{java_script}
</head>
<body {body_tags}>
{include_wz_tooltip}
<!-- END head -->
<!-- BEGIN framework -->
<egw-framework id="egw_fw_basecontainer" class="sl-theme-light"
application-list="{application-list}"
>
<a slot="logo" href="{logo_url}" target="_blank"><img src="{logo_header}" title="{logo_title}" alt="Site logo"/></a>
<div slot="header-right" id="egw_fw_topmenu_info_items">
{topmenu_info_items}
<script>
</script>
</div>
<!-- Fake apps -->
<sl-icon-button name="backpack" slot="header" label="Backpack application"></sl-icon-button>
<sl-icon-button name="airplane" slot="header" label="Airplaine application"></sl-icon-button>
<sl-icon-button name="mortarboard" slot="header" label="Mortarboard application"></sl-icon-button>
<et2-image src="mail/navbar" slot="header"></et2-image>
<div slot="aside" id="egw_fw_sidebar_r"></div>
<!-- Fake app -->
<egw-app name="fake app">
<div slot="banner">Something inside the app - main</div>
</egw-app>
</egw-framework>
{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">
{firstload_animation}
</div>
<!-- END framework -->

View File

@ -0,0 +1,119 @@
<?php
use EGroupware\Api;
class kdots_framework extends Api\Framework\Ajax
{
/**
* Appname used for everything but JS includes
*/
const APP = 'kdots';
/**
* Appname used to include javascript code
*/
const JS_INCLUDE_APP = 'kdots';
/**
* Enable to use this template sets login.tpl for login page
*/
const LOGIN_TEMPLATE_SET = true;
/**
* Get header as array to eg. set as vars for a template (from idots' head.inc.php)
*
* @param array $extra =array() extra attributes passed as data-attribute to egw.js
* @return array
*/
protected function _get_header(array $extra = array())
{
$data = parent::_get_header($extra);
$data['applicationlist'] = htmlentities(json_encode($extra['navbar-apps'], JSON_HEX_QUOT | JSON_HEX_AMP), ENT_QUOTES, 'UTF-8');
return $data;
}
function topmenu(array $vars, array $apps)
{
$this->topmenu_items = $this->topmenu_info_items = array();
parent::topmenu($vars, $apps);
$vars['topmenu_items'] = "<sl-menu>" . implode("\n", $this->topmenu_items) . "</sl-menu>";
$vars['topmenu_info_items'] = '';
foreach($this->topmenu_info_items as $id => $item)
{
switch($id)
{
case 'user_avatar':
$vars['topmenu_info_items'] .= "<sl-dropdown class=\"topmenu_info_item\" id=\"topmenu_info_{$id}\" aria-label='" . lang("User menu") . "' tabindex='0'><div slot='trigger'>$item</div> {$vars['topmenu_items']}</sl-dropdown>";
break;
default:
$vars['topmenu_info_items'] .= '<button class="topmenu_info_item"' .
(is_numeric($id) ? '' : ' id="topmenu_info_' . $id . '"') . '>' . $item . "</button>\n";
}
}
$this->topmenu_items = $this->topmenu_info_items = null;
return $vars;
}
/**
* Add info items to the topmenu template class to be displayed
*
* @param string $content Api\Html of item
* @param string $id = null
* @access protected
* @return void
*/
function _add_topmenu_info_item($content, $id = null)
{
if(strpos($content, 'menuaction=admin.admin_accesslog.sessions') !== false)
{
$content = preg_replace('/href="([^"]+)"/', "href=\"javascript:egw_link_handler('\\1','admin')\"", $content);
}
if($id)
{
$this->topmenu_info_items[$id] = $content;
}
else
{
$this->topmenu_info_items[] = $content;
}
}
/**
* Add menu items to the topmenu template class to be displayed
*
* @param array $app application data
* @param mixed $alt_label string with alternative menu item label default value = null
* @param string $urlextra string with alternate additional code inside <a>-tag
* @access protected
* @return void
*/
function _add_topmenu_item(array $app_data, $alt_label = null)
{
switch($app_data['name'])
{
case 'manual':
$app_data['url'] = "javascript:callManual();";
break;
default:
if(Api\Header\UserAgent::mobile() || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile')
{
break;
}
if(strpos($app_data['url'], 'logout.php') === false && substr($app_data['url'], 0, 11) != 'javascript:')
{
$app_data['url'] = "javascript:egw_link_handler('" . $app_data['url'] . "','" .
(isset($GLOBALS['egw_info']['user']['apps'][$app_data['name']]) ?
$app_data['name'] : 'about') . "')";
}
}
$id = $app_data['id'] ? $app_data['id'] : ($app_data['name'] ? $app_data['name'] : $app_data['title']);
$title = htmlspecialchars($alt_label ? $alt_label : $app_data['title']);
$this->topmenu_items[] = '<sl-menu-item id="topmenu_' . $id . '" value="' . htmlspecialchars($app_data['url']) . '" title="' . $app_data['title'] . '">' . $title . '</sl-menu-item>';
}
}

69
kdots/js/EgwApp.styles.ts Normal file
View File

@ -0,0 +1,69 @@
import {css} from 'lit';
export default css`
/* Layout */
:host {
position: relative;
width: 100%;
height: 100%;
display: grid;
}
:host > * {
position: relative;
display: flex;
}
.egw_fw_app__left {
grid-area: left;
overflow-x: hidden;
overflow-y: auto;
}
.egw_fw_app__right {
grid-area: right;
overflow-x: hidden;
overflow-y: auto;
}
.egw_fw_app__main {
grid-area: main;
overflow: hidden;
overflow-x: auto;
}
.egw_fw_app__header {
grid-area: header;
}
.egw_fw_app__footer {
grid-area: footer;
}
@media (min-width: 500px) {
:host {
grid-template-columns: [start left] fit-content(20%) [main] 1fr [right] fit-content(50%) [end];
grid-template-rows: [header] fit-content(2em) [main] 1fr [footer bottom] fit-content(2em) [end];
grid-template-areas:
"left-header header right-header"
"left main right"
"left-footer footer right-footer"
}
}
@media (max-width: 500px) {
:host {
grid-template-areas:
"header"
"main"
"left right"
}
[slot="footer"] {
display: none;
}
}
`

86
kdots/js/EgwApp.ts Normal file
View File

@ -0,0 +1,86 @@
import {css, html, LitElement} from "lit";
import {customElement} from "lit/decorators/custom-element.js";
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
import {property} from "lit/decorators/property.js";
import styles from "./EgwApp.styles";
import {state} from "lit/decorators/state.js";
@customElement('egw-app')
//@ts-ignore
export class EgwApp extends LitElement
{
static get styles()
{
return [
styles,
// TEMP STUFF
css`
:host {
--placeholder-background-color: #e97234;
}
.placeholder {
width: 100%;
display: block;
font-size: 200%;
text-align: center;
background-color: var(--placeholder-background-color, silver);
}
.placeholder:after {
content: " (placeholder)";
}
[class*="left"] .placeholder, [class*="right"] .placeholder {
background-color: color-mix(in lch, var(--placeholder-background-color), rgba(1, 1, 1, .5));
}
[class*="footer"] .placeholder {
background-color: color-mix(in lch, var(--placeholder-background-color), rgba(1, 1, 1, .05));
}
`
];
}
@property()
name = "Application name";
@state()
leftCollapsed = false;
get egw()
{
return window.egw ?? this.parentElement.egw ?? null;
}
render()
{
return html`
<div class="egw_fw_app__name" part="name">
<sl-icon-button name="${this.leftCollapsed ? "chevron-double-right" : "chevron-double-left"}"
label="${this.egw?.lang("Hide area")}"
@click=${() => {this.leftCollapsed = !this.leftCollapsed}}
></sl-icon-button>
<h2>${this.egw?.lang(this.name) ?? this.name}</h2>
</div>
<aside class="egw_fw_app__left" part="left">
<slot name="left"><span class="placeholder">left</span></slot>
</aside>
<aside class="egw_fw_app__right" part="right">
<slot name="right"><span class="placeholder">right</span></slot>
</aside>
<header class="egw_fw_app__header" part="header">
<slot name="main-header"><span class="placeholder"> ${this.name} main-header</span></slot>
</header>
<main class="egw_fw_app__main" part="main"
aria-label="${this.name}" tabindex="0">
<slot><span class="placeholder">main</span></slot>
</main>
<footer class="egw_fw_app__footer" part="footer">
<slot name="footer"><span class="placeholder">main-footer</span></slot>
</footer>
`;
}
}

View File

@ -0,0 +1,112 @@
import {css} from 'lit';
export default css`
:host {
display: block;
width: 100vw;
height: 100vh;
position: relative;
}
.egw_fw__layout-default {
display: grid;
gap: 0.5em 0.1em;
border: 1px dotted;
width: 100%;
height: 100%;
}
.egw_fw__layout-default > * {
position: relative;
display: flex;
}
.egw_fw__layout-default .egw_fw__banner {
grid-area: banner;
grid-column-start: banner-start;
grid-column-end: banner-end;
}
.egw_fw__layout-default .egw_fw__header {
grid-area: header;
align-items: center;
}
/* To use the sl-split-panel, we need it to have its own space & nest stuff inside */
.egw_fw__layout-default .egw_fw__divider {
grid-column-start: sidemenu-start;
grid-column-end: status-start;
grid-row-start: main-header;
grid-row-end: footer;
display: flex;
justify-content: stretch;
}
.egw_fw__layout-default sl-split-panel {
width: 100%;
}
.egw_fw__layout-default sl-split-panel::part(divider) {
color: var(--sl-color-primary-500);
}
.egw_fw__layout-default .egw_fw__sidemenu {
overflow-x: hidden;
overflow-y: auto;
}
.egw_fw__layout-default .egw_fw__status {
overflow-x: hidden;
overflow-y: auto;
}
.egw_fw__layout-default .egw_fw__main-wrapper {
width: 100%;
display: grid;
grid-template-columns: [start] 1fr [end];
grid-template-rows: [top main-header] fit-content(2em) [main] 1fr [main-footer] fit-content(0px) [ bottom]
}
.egw_fw__layout-default .egw_fw__main {
grid-column-start: start;
grid-column-end: end;
grid-row-start: main;
grid-row-end: main;
overflow: hidden;
overflow-x: auto;
}
.egw_fw__layout-default .egw_fw__main-header {
grid-column-start: start;
grid-column-end: end;
grid-row-start: main-header;
grid-row-end: main-header
}
.egw_fw__layout-default .egw_fw__main-footer {
grid-column-start: start;
grid-column-end: end;
grid-row-start: main-footer;
grid-row-end: main-footer
}
.egw_fw__layout-default .egw_fw__footer {
grid-area: footer;
}
@media (min-width: 500px) {
.egw_fw__layout-default {
grid-template-columns: [start sidemenu-start banner-start header-start footer-start] 200px [sidemenu-end main-start] 1fr [main-end] fit-content(2em) [header-end banner-end end];
grid-template-rows: [top banner] fit-content(2em) [header] fit-content(2em) [ main-header] fit-content(2em) [main] 1fr [main-footer] fit-content(2em) [footer bottom] fit-content(2em);
}
}
/* Actual styles */
.egw_fw__header sl-icon-button {
color: inherit;
}
`

131
kdots/js/EgwFramework.ts Normal file
View File

@ -0,0 +1,131 @@
import {css, html, LitElement} from "lit";
import {customElement} from "lit/decorators/custom-element.js";
import {property} from "lit/decorators/property.js";
import {classMap} from "lit/directives/class-map.js";
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
import styles from "./EgwFramework.styles";
import {repeat} from "lit/directives/repeat.js";
@customElement('egw-framework')
//@ts-ignore
export class EgwFramework extends LitElement
{
static get styles()
{
return [
styles,
// TEMP STUFF
css`
.placeholder {
width: 100%;
display: block;
font-size: 200%;
text-align: center;
background-color: var(--placeholder-background-color, silver);
}
.placeholder:after {
content: " (placeholder)";
}
.egw_fw__base {
--placeholder-background-color: #75bd20;
}
.egw_fw__footer .placeholder {
background-color: hsl(182, 58%, 62%);
}
.egw_fw__main-wrapper {
--placeholder-background-color: #e97234;
}
.egw_fw__status .placeholder {
writing-mode: vertical-rl;
text-orientation: mixed;
height: 100%;
}
[class*="left"] .placeholder {
background-color: color-mix(in lch, var(--placeholder-background-color), rgba(1, 1, 1, .5));
}
[class*="footer"] .placeholder {
background-color: color-mix(in lch, var(--placeholder-background-color), rgba(1, 1, 1, .05));
}
::slotted(div#egw_fw_sidebar_r) {
position: relative;
}
`
];
}
@property()
layout = "default";
@property({type: Array})
applicationList = [];
get egw()
{
return window.egw ?? {
app_name: () => "",
lang: (t) => t,
preference: (n, app) => ""
};
}
render()
{
const statusPosition = this.egw?.preference("statusPosition", this.egw?.app_name()) ?? "36";
const classes = {
"egw_fw__base": true
}
classes[`egw_fw__layout-${this.layout}`] = true;
return html`
<div class=${classMap(classes)} part="base">
<div class="egw_fw__banner" part="banner" role="banner">
<slot name="banner"><span class="placeholder">Banner</span></slot>
</div>
<header class="egw_fw__header" part="header">
<slot name="logo"></slot>
<sl-dropdown class="egw_fw__app_list">
<sl-icon-button slot="trigger" name="grid-3x3-gap"
label="${this.egw.lang("Application list")}"
aria-description="${this.egw.lang("Activate for a list of applications")}"
></sl-icon-button>
${repeat(this.applicationList, (app) => html`
<et2-image src="${app.icon}" aria-label="${app.title}"></et2-image>`)}
</sl-dropdown>
<slot name="header"><span class="placeholder">header</span></slot>
<slot name="header-right"><span class="placeholder">header-right</span></slot>
</header>
<div class="egw_fw__divider">
<sl-split-panel part="status-split" position-in-pixels="${statusPosition}" primary="end"
snap="150px 45px 0px"
snap-threshold="40"
aria-label="Side menu resize">
<main slot="start" part="main">
<slot></slot>
</main>
<sl-icon slot="divider" name="grip-vertical"></sl-icon>
<aside slot="end" class="egw_fw__status" part="status">
<slot name="status"><span class="placeholder">status</span></slot>
</aside>
</sl-split-panel>
</div>
<footer class="egw_fw__footer" part="footer">
<slot name="footer"><span class="placeholder">footer</span></slot>
</footer>
</div>
`;
}
}

17
kdots/js/app.ts Normal file
View File

@ -0,0 +1,17 @@
/**
* app.ts is auto-built
*/
import "./EgwFramework";
import "./EgwApp";
document.addEventListener('DOMContentLoaded', () =>
{
/* Set up listener on avatar menu */
const avatarMenu = document.querySelector("#topmenu_info_user_avatar");
avatarMenu.addEventListener("sl-select", (e : CustomEvent) =>
{
window.egw.open_link(e.detail.item.value);
});
});

32
kdots/setup/setup.inc.php Normal file
View File

@ -0,0 +1,32 @@
<?php
/**
* EGroupware: Standard template
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
$GLOBALS['egw_info']['template']['kdots']['name'] = 'kdots';
$GLOBALS['egw_info']['template']['kdots']['title'] = 'Kdots WIP ';
$GLOBALS['egw_info']['template']['kdots']['version'] = '24.1';
$GLOBALS['egw_info']['template']['kdots']['author'] = array(
array('name' => 'EGroupware GmbH', 'url' => 'http://www.egroupware.org/'),
);
$GLOBALS['egw_info']['template']['kdots']['license'] = 'GPL';
$GLOBALS['egw_info']['template']['kdots']['maintainer'] = array(
array('name' => 'EGroupware GmbH', 'url' => 'http://www.egroupware.org/')
);
$GLOBALS['egw_info']['template']['kdots']['description'] = "WIP framework of EGroupware.";
$GLOBALS['egw_info']['template']['kdots']['windowed'] = true;
// specify (different) labels for default themes
$GLOBALS['egw_info']['template']['kdots']['themes'] = array(
'default' => 'Standard'
);
// Dependencies for this template to work
$GLOBALS['egw_info']['template']['kdots']['depends'][] = array(
'appname' => 'api',
'versions' => array('23.1')
);