Change tabs to use Shoelace

Includes changes to historylog, since it did some deferred loading & sizing magic based on tab
This commit is contained in:
nathan 2022-08-02 10:30:40 -06:00
parent 358a6a8ac8
commit db143f047a
6 changed files with 525 additions and 72 deletions

View File

@ -0,0 +1,23 @@
import {Et2Widget} from "../../Et2Widget/Et2Widget";
import {SlTab} from "@shoelace-style/shoelace";
export class Et2Tab extends Et2Widget(SlTab)
{
static get properties()
{
return {
...super.properties,
hidden: {type: Boolean, reflect: true}
}
}
constructor()
{
super();
this.hidden = false;
}
}
customElements.define("et2-tab", Et2Tab);

View File

@ -0,0 +1,52 @@
import {Et2Widget} from "../../Et2Widget/Et2Widget";
import {SlTabPanel} from "@shoelace-style/shoelace";
import shoelace from "../../Styles/shoelace";
import {css} from "@lion/core";
export class Et2TabPanel extends Et2Widget(SlTabPanel)
{
static get styles()
{
return [
// @ts-ignore
...super.styles,
...shoelace,
css`
:host {
height: 100%;
/*
width: 100%;
min-height: fit-content;
min-width: fit-content;
*/
}
.tab-panel {
height: 100%;
}
::slotted(*) {
height: 100%;
}
`
];
}
static get properties()
{
return {
...super.properties,
hidden: {type: Boolean, reflect: true}
}
}
constructor()
{
super();
this.hidden = false;
}
}
customElements.define("et2-tab-panel", Et2TabPanel);

View File

@ -0,0 +1,388 @@
/**
* EGroupware eTemplate2 - Box widget
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {SlTab, SlTabGroup, SlTabPanel} from "@shoelace-style/shoelace";
import {Et2Widget, loadWebComponent} from "../../Et2Widget/Et2Widget";
import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault} from "../../et2_core_xml";
import {css, PropertyValues} from "@lion/core";
import shoelace from "../../Styles/shoelace";
export class Et2Tabs extends Et2Widget(SlTabGroup)
{
static get styles()
{
return [
...super.styles,
...shoelace,
css`
.tab-group--top {
height: 100%;
min-height: fit-content;
}
.tab-group__body {
flex: 1 1 auto;
overflow: hidden auto;
}
.tab-group__body-fixed-height {
flex: 0 0 auto;
}
::slotted([hidden]) {
display: none;
}
::slotted(et2-tab-panel) {
flex: 1 1 auto;
}
`
];
}
static get properties()
{
return {
...super.properties,
/**
* Array of [extra] tabs.
* Each tab needs {label:..., template:...}.
* Additional optional keys are prepend, hidden and id, for access into content array
*/
extraTabs: {type: Object},
/**
* Add tabs to template
* Set to true if tabs specified in tabs property should be added to tabs read from template,
* default false if not which replaces what's in template
*/
addTabs: {type: Boolean},
/**
* Set the height for tabs
* Leave unset to size automatically
*/
tabHeight: {type: String},
/**
* @deprecated use "placement" instead
* @see https://shoelace.style/components/tab-group
*/
alignTabs: {type: String}
}
}
/**
* Index of currently selected tab
* @type {number}
* @private
*/
protected _selectedIndex = -1;
protected tabData = [];
protected lazyLoaded = false;
constructor()
{
super();
this.extraTabs = [];
this.addTabs = false;
}
loadFromXML(_node)
{
// Get the tabs and tabpanels tags
const tabsElems = et2_directChildrenByTagName(_node, "tabs");
const tabpanelsElems = et2_directChildrenByTagName(_node, "tabpanels");
const tabData = [];
// Check for a parent height, we'll apply it to tab panels
var height = et2_readAttrWithDefault(_node.parentNode, "height", null);
if(height)
{
this.tabContainer.css("height", height);
}
// if no tabs set or they should be added to tabs from xml
if(!this.extraTabs || this.extraTabs.length == 0 || this.addTabs)
{
if(tabsElems.length == 1 && tabpanelsElems.length == 1)
{
var tabs = tabsElems[0];
var tabpanels = tabpanelsElems[0];
// Parse the "tabs" tag
this._readTabs(tabData, tabs);
// Read and create the widgets defined in the "tabpanels"
this._readTabPanels(tabData, tabpanels);
}
else
{
this.egw().debug("error", "Error while parsing tabbox, none or multiple tabs or tabpanels tags!", this);
}
}
// Add in additional tabs
if(this.extraTabs)
{
let readonly = this.getArrayMgr("readonlys").getEntry(this.id) || {};
for(let i = 0; i < this.extraTabs.length; i++)
{
let tab = this.extraTabs[i];
let tab_id = tab.id || tab.template;
let tab_options = {id: tab_id, template: tab.template, url: tab.url, content: undefined};
if(tab.id)
{
tab_options.content = tab.id;
}
tabData[tab.prepend ? 'unshift' : 'push'].call(tabData, {
"id": tab_id,
"label": this.egw().lang(tab.label),
"widget": null,
"widget_options": tab_options,
"contentDiv": null,
"flagDiv": null,
"hidden": typeof tab.hidden != "undefined" ? tab.hidden : readonly[tab_id] || false,
"XMLNode": null,
"promise": null
});
}
}
// Create the tab DOM-Nodes
this.createTabs(tabData);
}
_readTabs(tabData, tabs)
{
let selected = "";
this._selectedIndex = -1;
let hidden = {};
if(this.id)
{
// Set the value for this element
let contentMgr = this.getArrayMgr("content");
if(contentMgr != null)
{
let val = contentMgr.getEntry(this.id);
if(val !== null)
{
selected = val;
}
}
contentMgr = this.getArrayMgr("readonlys");
if(contentMgr != null)
{
let val = contentMgr.getEntry(this.id);
if(val !== null && typeof val !== 'undefined')
{
hidden = val;
}
}
}
let i = 0;
et2_filteredNodeIterator(tabs, function(node, nodeName)
{
if(nodeName == "tab")
{
const index_name = et2_readAttrWithDefault(node, "id", '');
var hide = false;
var widget_options = {};
if(index_name)
{
if(selected == index_name)
{
this.selected_index = i;
}
if(hidden[index_name])
{
hide = true;
}
// Get the class attribute and add it as widget_options
const classAttr = et2_readAttrWithDefault(node, "class", '');
if(classAttr)
{
widget_options = {'class': classAttr};
}
}
tabData.push({
"id": index_name,
"label": this.egw().lang(et2_readAttrWithDefault(node, "label", "Tab")),
"widget": null,
"widget_options": widget_options,
"contentDiv": null,
"flagDiv": null,
"hidden": hide,
"XMLNode": null,
"promise": null
});
}
else
{
throw("Error while parsing: Invalid tag '" + nodeName +
"' in tabs tag");
}
i++;
}, this);
// Make sure we don't try to display a hidden tab
for(let i = 0; i < tabData.length && this._selectedIndex < 0; i++)
{
if(!tabData[i].hidden)
{
this._selectedIndex = i;
}
}
}
_readTabPanels(tabData, tabpanels)
{
var i = 0;
et2_filteredNodeIterator(tabpanels, function(node, nodeName)
{
if(i < tabData.length)
{
// Store node for later evaluation
tabData[i].XMLNode = node;
}
else
{
throw("Error while reading tabpanels tag, too many widgets!");
}
i++;
}, this);
}
protected update(changedProperties : PropertyValues)
{
super.update(changedProperties);
if(changedProperties.has("tabHeight"))
{
const body = this.shadowRoot.querySelector(".tab-group__body");
body.style.setProperty("height", this.tabHeight == parseInt(this.tabHeight) + "" ? this.tabHeight + "px" : this.tabHeight);
body.classList.toggle("tab-group__body-fixed-height", this.tabHeight !== '');
}
}
/**
* Create the nodes for tabs
*
* @param tabData
* @protected
*/
protected createTabs(tabData)
{
this.tabData = tabData;
tabData.forEach((tab, index) =>
{
// Tab - SlTabGroup looks for sl-tab, so we can't use our own without overriding a lot
tab.flagDiv = loadWebComponent("et2-tab", {
slot: "nav",
panel: tab.id,
active: index == this._selectedIndex,
hidden: tab.hidden
}, this);
// Set tab label
tab.flagDiv.appendChild(document.createTextNode(tab.label));
});
tabData.forEach((tab, index) =>
{
this.createPanel(tab);
});
}
protected createPanel(tab, active = false)
{
// Tab panel
tab.contentDiv = loadWebComponent('et2-tab-panel', {
name: tab.id,
active: active,
hidden: tab.hidden
}, this);
// Tab content
let tabContent = tab.contentDiv.createElementFromNode(tab.XMLNode);
tab.contentDiv.appendChild(
typeof window.customElements.get(tab.XMLNode.nodeName) == "undefined" ?
tabContent.getDOMNode() : tabContent
);
return tab.contentDiv;
}
getAllTabs(includeDisabled = false)
{
const slot = <HTMLSlotElement>this.shadowRoot.querySelector('slot[name="nav"]');
const tabNames = ["sl-tab", "et2-tab"];
return <SlTab[]>[...slot.assignedElements()].filter((el) =>
{
return includeDisabled ? tabNames.indexOf(el.tagName.toLowerCase()) != -1 : tabNames.indexOf(el.tagName.toLowerCase()) !== -1 && !el.disabled;
});
}
getAllPanels()
{
const slot = this.body.querySelector('slot')!;
return [...slot.assignedElements()].filter(el => ['et2-tab-panel', 'sl-tab-panel'].indexOf(el.tagName.toLowerCase()) != -1) as [SlTabPanel];
}
handleClick(event : MouseEvent)
{
const target = event.target as HTMLElement;
const tab = target.closest('et2-tab');
const tabGroup = tab?.closest('sl-tab-group') || tab?.closest('et2-tabbox');
// Ensure the target tab is in this tab group
if(tabGroup !== this)
{
return;
}
if(tab !== null)
{
this.setActiveTab(tab, {scrollBehavior: 'smooth'});
}
}
/**
* Set up for printing
*
* @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up
* (waiting for data)
*/
beforePrint()
{
// Remove the "active" flag from all tabs-flags
this.querySelector("[active]").removeAttribute("active");
// Remove height limit
this.style.height = '';
// Show all enabled tabs
for(let i = 0; i < this.tabData.length; i++)
{
let entry = this.tabData[i];
if(entry.hidden)
{
continue;
}
entry.flagDiv.insertBefore(entry.contentDiv);
entry.contentDiv.show();
}
}
/**
* Reset after printing
*/
afterPrint()
{
this.setActiveTab(this._selectedIndex);
}
}
customElements.define("et2-tabbox", Et2Tabs);

View File

@ -18,11 +18,18 @@ import {ClassWithAttributes} from './et2_core_inheritance';
import {et2_IDOMNode} from "./et2_core_interfaces"; import {et2_IDOMNode} from "./et2_core_interfaces";
import {et2_hasChild, et2_no_init} from "./et2_core_common"; import {et2_hasChild, et2_no_init} from "./et2_core_common";
import {et2_widget, WidgetConfig} from "./et2_core_widget"; import {et2_widget, WidgetConfig} from "./et2_core_widget";
import {egw_getActionManager, egwActionObject, egwActionObjectInterface, egw_getAppObjectManager,egw_getObjectManager} from '../egw_action/egw_action.js'; import {
import {EGW_AI_DRAG_OVER, EGW_AI_DRAG_OUT} from '../egw_action/egw_action_constants.js'; egw_getActionManager,
egw_getAppObjectManager,
egw_getObjectManager,
egwActionObject,
egwActionObjectInterface
} from '../egw_action/egw_action.js';
import {EGW_AI_DRAG_OUT, EGW_AI_DRAG_OVER} from '../egw_action/egw_action_constants.js';
import {egw} from "../jsapi/egw_global"; import {egw} from "../jsapi/egw_global";
// fixing circular dependencies by only importing type import {Et2Tab} from "./Layout/Et2Tabs/Et2Tab";
import type {et2_tabbox} from "./et2_widget_tabs"; import {Et2TabPanel} from "./Layout/Et2Tabs/Et2TabPanel";
import {Et2Tabs} from "./Layout/Et2Tabs/Et2Tabs";
/** /**
* Abstract widget class which can be inserted into the DOM. All widget classes * Abstract widget class which can be inserted into the DOM. All widget classes
@ -320,12 +327,14 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
* *
* @returns {Object|null} Data for tab the widget is on * @returns {Object|null} Data for tab the widget is on
*/ */
get_tab_info() : {id: string, label: string, widget: et2_widget, contentDiv: JQuery, flagDiv: JQuery} | null get_tab_info() : { id : string, label : string, widget : et2_widget, contentDiv : Et2TabPanel, flagDiv : Et2Tab } | null
{ {
var parent : et2_widget = this; var parent : et2_widget = this;
do { do
{
parent = parent.getParent(); parent = parent.getParent();
} while (parent !== this.getRoot() && parent.getType() !== 'tabbox'); }
while(parent !== this.getRoot() && ['tabbox', 'ET2-TABBOX'].indexOf(parent.getType()) == -1);
// No tab // No tab
if(parent === this.getRoot()) if(parent === this.getRoot())
@ -333,14 +342,14 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
return null; return null;
} }
let tabbox : et2_tabbox = <et2_tabbox><unknown>parent; let tabbox : Et2Tabs = <Et2Tabs><unknown>parent;
// Find the tab index // Find the tab index
for(var i = 0; i < tabbox.tabData.length; i++) for(var i = 0; i < tabbox.tabData.length; i++)
{ {
// Find the tab by DOM heritage // Find the tab by DOM heritage
// @ts-ignore // @ts-ignore
if(tabbox.tabData[i].contentDiv.has(this.div).length) if(tabbox.tabData[i].contentDiv.contains(this.div[0] || this))
{ {
return tabbox.tabData[i]; return tabbox.tabData[i];
} }

View File

@ -27,7 +27,6 @@ import {et2_dataview_column} from "./et2_dataview_model_columns";
import {et2_dataview_controller} from "./et2_dataview_controller"; import {et2_dataview_controller} from "./et2_dataview_controller";
import {et2_diff} from "./et2_widget_diff"; import {et2_diff} from "./et2_widget_diff";
import {et2_IDetachedDOM, et2_IResizeable} from "./et2_core_interfaces"; import {et2_IDetachedDOM, et2_IResizeable} from "./et2_core_interfaces";
import {et2_dynheight} from "./et2_widget_dynheight";
import {et2_customfields_list} from "./et2_extension_customfields"; import {et2_customfields_list} from "./et2_extension_customfields";
import {et2_selectbox} from "./et2_widget_selectbox"; import {et2_selectbox} from "./et2_widget_selectbox";
import {loadWebComponent} from "./Et2Widget/Et2Widget"; import {loadWebComponent} from "./Et2Widget/Et2Widget";
@ -96,7 +95,6 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider
private div: JQuery; private div: JQuery;
private innerDiv: JQuery; private innerDiv: JQuery;
private _filters: { appname: string; record_id: string; get_rows: string; }; private _filters: { appname: string; record_id: string; get_rows: string; };
private dynheight: et2_dynheight;
private dataview: et2_dataview; private dataview: et2_dataview;
private controller: et2_dataview_controller; private controller: et2_dataview_controller;
private fields: any; private fields: any;
@ -114,6 +112,8 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider
this.innerDiv = jQuery(document.createElement("div")) this.innerDiv = jQuery(document.createElement("div"))
.appendTo(this.div); .appendTo(this.div);
this._resize = this._resize.bind(this);
} }
set_status_id( _new_id) set_status_id( _new_id)
@ -130,36 +130,16 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider
if(tab) if(tab)
{ {
// Bind the action to when the tab is selected // Bind the action to when the tab is selected
const handler = function(e) const handler = (e) =>
{ {
e.data.div.unbind("click.history"); if(typeof this.dataview == "undefined")
// Bind on click tap, because we need to update history size {
// after a rezise happend and history log was not the active tab this.finishInit();
e.data.div.bind("click.history", {"history": e.data.history, div: tab.flagDiv}, function (e) {
if (e.data.history && e.data.history.dynheight) {
e.data.history.dynheight.update(function (_w, _h) {
e.data.history.dataview.resize(_w, _h);
});
}
});
if (typeof e.data.history.dataview == "undefined") {
e.data.history.finishInit();
if (e.data.history.dynheight) {
e.data.history.dynheight.update(function (_w, _h) {
e.data.history.dataview.resize(_w, _h);
});
}
} }
// TODO: Find a better way to get this to wait
window.setTimeout(this._resize, 10);
}; };
tab.flagDiv.bind("click.history",{"history": this, div: tab.flagDiv}, handler); tab.flagDiv.addEventListener("click", handler);
// Display if history tab is selected
if(tab.contentDiv.is(':visible') && typeof this.dataview == 'undefined')
{
tab.flagDiv.trigger("click.history");
}
} }
else else
{ {
@ -194,13 +174,7 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider
this.egw().debug("warn", "status_id attribute should not be the same as historylog ID"); this.egw().debug("warn", "status_id attribute should not be the same as historylog ID");
} }
// Create the dynheight component which dynamically scales the inner
// container.
this.div.parentsUntil('.et2_tabs').height('100%');
const parent = this.get_tab_info(); const parent = this.get_tab_info();
this.dynheight = new et2_dynheight(parent ? parent.contentDiv : this.div.parent(),
this.innerDiv, 250
);
// Create the outer grid container // Create the outer grid container
this.dataview = new et2_dataview(this.innerDiv, this.egw()); this.dataview = new et2_dataview(this.innerDiv, this.egw());
@ -267,9 +241,7 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider
// Register a resize callback // Register a resize callback
jQuery(window).on('resize.' +this.options.value.app + this.options.value.id, function() jQuery(window).on('resize.' +this.options.value.app + this.options.value.id, function()
{ {
if (this && typeof this.dynheight != 'undefined') this.dynheight.update(function(_w, _h) { this.dataview.resize();
this.dataview.resize(_w, _h);
}.bind(this));
}.bind(this)); }.bind(this));
} }
@ -297,7 +269,6 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider
// Free the grid components // Free the grid components
if(this.dataview) this.dataview.destroy(); if(this.dataview) this.dataview.destroy();
if(this.controller) this.controller.destroy(); if(this.controller) this.controller.destroy();
if(this.dynheight) this.dynheight.destroy();
super.destroy(); super.destroy();
} }
@ -750,37 +721,33 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider
row.parents("td").attr("colspan", 2) row.parents("td").attr("colspan", 2)
.css("border-right", "none"); .css("border-right", "none");
row.css("width", ( row.css("width", (
this.dataview.getColumnMgr().getColumnWidth(et2_historylog.NEW_VALUE) + this.dataview.getColumnMgr().getColumnWidth(et2_historylog.NEW_VALUE) +
this.dataview.getColumnMgr().getColumnWidth(et2_historylog.OLD_VALUE)-10)+'px'); this.dataview.getColumnMgr().getColumnWidth(et2_historylog.OLD_VALUE) - 10) + 'px');
// Skip column 5 // Skip column 5
row.parents("td").next().remove(); row.parents("td").next().remove();
} }
_resize()
{
let tab = this.get_tab_info();
if(this.dataview)
{
// -# to avoid scrollbars
this.dataview.resize(
Math.min(
window.innerWidth - 15,
parseInt(getComputedStyle(tab.contentDiv).width)
) - 5,
parseInt(getComputedStyle(tab.contentDiv).height) - 5
);
}
}
resize(_height) resize(_height)
{ {
if (typeof this.options != 'undefined' && _height
&& typeof this.options.resize_ratio != 'undefined') this._resize();
{
// apply the ratio
_height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height;
if (_height != 0)
{
// 250px is the default value for history widget
// if it's not loaded yet and window is resized
// then add the default height with excess_height
if (this.div.height() == 0) _height += 250;
this.div.height(this.div.height() + _height);
// trigger the history registered resize
// in order to update the height with new value
this.div.trigger('resize.' +this.options.value.app + this.options.value.id);
}
}
if(this.dynheight)
{
this.dynheight.update();
}
// Resize diff widgets to match new space // Resize diff widgets to match new space
if(this.dataview) if(this.dataview)
{ {

View File

@ -25,6 +25,9 @@ import '../jsapi/egw_json.js';
import {egwIsMobile} from "../egw_action/egw_action_common.js"; import {egwIsMobile} from "../egw_action/egw_action_common.js";
import './Layout/Et2Box/Et2Box'; import './Layout/Et2Box/Et2Box';
import './Layout/Et2Details/Et2Details'; import './Layout/Et2Details/Et2Details';
import './Layout/Et2Tabs/Et2Tab';
import './Layout/Et2Tabs/Et2Tabs';
import './Layout/Et2Tabs/Et2TabPanel';
import './Et2Avatar/Et2Avatar'; import './Et2Avatar/Et2Avatar';
import './Et2Button/Et2Button'; import './Et2Button/Et2Button';
import './Et2Checkbox/Et2Checkbox'; import './Et2Checkbox/Et2Checkbox';
@ -666,6 +669,8 @@ export class etemplate2
if(egw.debug_level() >= 4 && console.timeStamp) if(egw.debug_level() >= 4 && console.timeStamp)
{ {
console.timeStamp("Begin rendering template"); console.timeStamp("Begin rendering template");
console.time("Template load");
console.time("loadFromXML");
} }
// Add into indexed list - do this before, so anything looking can find it, // Add into indexed list - do this before, so anything looking can find it,
@ -678,6 +683,8 @@ export class etemplate2
// Read the XML structure of the requested template // Read the XML structure of the requested template
this._widgetContainer.loadFromXML(etemplate2.templates[this.name]); this._widgetContainer.loadFromXML(etemplate2.templates[this.name]);
console.timeEnd("loadFromXML");
console.time("deferred");
// List of Promises from widgets that are not quite fully loaded // List of Promises from widgets that are not quite fully loaded
const deferred = []; const deferred = [];
@ -693,6 +700,10 @@ export class etemplate2
if(egw.debug_level() >= 3 && console.groupEnd) if(egw.debug_level() >= 3 && console.groupEnd)
{ {
if(console.timeStamp)
{
console.timeStamp("loading finished, waiting for deferred");
}
egw.window.console.groupEnd(); egw.window.console.groupEnd();
} }
@ -702,6 +713,9 @@ export class etemplate2
{ {
Promise.all(deferred).then(() => Promise.all(deferred).then(() =>
{ {
console.timeEnd("deferred");
console.timeStamp("Deferred done");
// Clear dirty now that it's all loaded // Clear dirty now that it's all loaded
this.widgetContainer.iterateOver((_widget) => this.widgetContainer.iterateOver((_widget) =>
{ {