diff --git a/api/js/etemplate/Layout/Et2Tabs/Et2Tab.ts b/api/js/etemplate/Layout/Et2Tabs/Et2Tab.ts new file mode 100644 index 0000000000..14d9e778b3 --- /dev/null +++ b/api/js/etemplate/Layout/Et2Tabs/Et2Tab.ts @@ -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); \ No newline at end of file diff --git a/api/js/etemplate/Layout/Et2Tabs/Et2TabPanel.ts b/api/js/etemplate/Layout/Et2Tabs/Et2TabPanel.ts new file mode 100644 index 0000000000..08ccbc0b20 --- /dev/null +++ b/api/js/etemplate/Layout/Et2Tabs/Et2TabPanel.ts @@ -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); \ No newline at end of file diff --git a/api/js/etemplate/Layout/Et2Tabs/Et2Tabs.ts b/api/js/etemplate/Layout/Et2Tabs/Et2Tabs.ts new file mode 100644 index 0000000000..320cbd04cd --- /dev/null +++ b/api/js/etemplate/Layout/Et2Tabs/Et2Tabs.ts @@ -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 = this.shadowRoot.querySelector('slot[name="nav"]'); + const tabNames = ["sl-tab", "et2-tab"]; + return [...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); \ No newline at end of file diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index 51279b2d8b..00264c4411 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -18,11 +18,18 @@ import {ClassWithAttributes} from './et2_core_inheritance'; import {et2_IDOMNode} from "./et2_core_interfaces"; import {et2_hasChild, et2_no_init} from "./et2_core_common"; import {et2_widget, WidgetConfig} from "./et2_core_widget"; -import {egw_getActionManager, egwActionObject, egwActionObjectInterface, egw_getAppObjectManager,egw_getObjectManager} from '../egw_action/egw_action.js'; -import {EGW_AI_DRAG_OVER, EGW_AI_DRAG_OUT} from '../egw_action/egw_action_constants.js'; +import { + 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"; -// fixing circular dependencies by only importing type -import type {et2_tabbox} from "./et2_widget_tabs"; +import {Et2Tab} from "./Layout/Et2Tabs/Et2Tab"; +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 @@ -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 */ - 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; - do { + do + { parent = parent.getParent(); - } while (parent !== this.getRoot() && parent.getType() !== 'tabbox'); + } + while(parent !== this.getRoot() && ['tabbox', 'ET2-TABBOX'].indexOf(parent.getType()) == -1); // No tab if(parent === this.getRoot()) @@ -333,14 +342,14 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode return null; } - let tabbox : et2_tabbox = parent; + let tabbox : Et2Tabs = parent; // Find the tab index for(var i = 0; i < tabbox.tabData.length; i++) { // Find the tab by DOM heritage // @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]; } diff --git a/api/js/etemplate/et2_widget_historylog.ts b/api/js/etemplate/et2_widget_historylog.ts index eb3203fcb1..f57e86c514 100644 --- a/api/js/etemplate/et2_widget_historylog.ts +++ b/api/js/etemplate/et2_widget_historylog.ts @@ -27,7 +27,6 @@ import {et2_dataview_column} from "./et2_dataview_model_columns"; import {et2_dataview_controller} from "./et2_dataview_controller"; import {et2_diff} from "./et2_widget_diff"; 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_selectbox} from "./et2_widget_selectbox"; import {loadWebComponent} from "./Et2Widget/Et2Widget"; @@ -96,7 +95,6 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider private div: JQuery; private innerDiv: JQuery; private _filters: { appname: string; record_id: string; get_rows: string; }; - private dynheight: et2_dynheight; private dataview: et2_dataview; private controller: et2_dataview_controller; private fields: any; @@ -114,6 +112,8 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider this.innerDiv = jQuery(document.createElement("div")) .appendTo(this.div); + + this._resize = this._resize.bind(this); } set_status_id( _new_id) @@ -130,36 +130,16 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider if(tab) { // Bind the action to when the tab is selected - const handler = function(e) + const handler = (e) => { - e.data.div.unbind("click.history"); - // Bind on click tap, because we need to update history size - // after a rezise happend and history log was not the active tab - 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); - }); - } + if(typeof this.dataview == "undefined") + { + this.finishInit(); } - + // 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); - - // Display if history tab is selected - if(tab.contentDiv.is(':visible') && typeof this.dataview == 'undefined') - { - tab.flagDiv.trigger("click.history"); - } + tab.flagDiv.addEventListener("click", handler); } 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"); } - // Create the dynheight component which dynamically scales the inner - // container. - this.div.parentsUntil('.et2_tabs').height('100%'); 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 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 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(_w, _h); - }.bind(this)); + this.dataview.resize(); }.bind(this)); } @@ -297,7 +269,6 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider // Free the grid components if(this.dataview) this.dataview.destroy(); if(this.controller) this.controller.destroy(); - if(this.dynheight) this.dynheight.destroy(); super.destroy(); } @@ -750,37 +721,33 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider row.parents("td").attr("colspan", 2) .css("border-right", "none"); row.css("width", ( - this.dataview.getColumnMgr().getColumnWidth(et2_historylog.NEW_VALUE) + - this.dataview.getColumnMgr().getColumnWidth(et2_historylog.OLD_VALUE)-10)+'px'); + this.dataview.getColumnMgr().getColumnWidth(et2_historylog.NEW_VALUE) + + this.dataview.getColumnMgr().getColumnWidth(et2_historylog.OLD_VALUE) - 10) + 'px'); // Skip column 5 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) { - if (typeof this.options != 'undefined' && _height - && typeof this.options.resize_ratio != 'undefined') - { - // 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(); - } + + this._resize(); // Resize diff widgets to match new space if(this.dataview) { diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 2b2a6183a8..48274f5160 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -25,6 +25,9 @@ import '../jsapi/egw_json.js'; import {egwIsMobile} from "../egw_action/egw_action_common.js"; import './Layout/Et2Box/Et2Box'; import './Layout/Et2Details/Et2Details'; +import './Layout/Et2Tabs/Et2Tab'; +import './Layout/Et2Tabs/Et2Tabs'; +import './Layout/Et2Tabs/Et2TabPanel'; import './Et2Avatar/Et2Avatar'; import './Et2Button/Et2Button'; import './Et2Checkbox/Et2Checkbox'; @@ -666,6 +669,8 @@ export class etemplate2 if(egw.debug_level() >= 4 && console.timeStamp) { 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, @@ -678,6 +683,8 @@ export class etemplate2 // Read the XML structure of the requested template this._widgetContainer.loadFromXML(etemplate2.templates[this.name]); + console.timeEnd("loadFromXML"); + console.time("deferred"); // List of Promises from widgets that are not quite fully loaded const deferred = []; @@ -693,6 +700,10 @@ export class etemplate2 if(egw.debug_level() >= 3 && console.groupEnd) { + if(console.timeStamp) + { + console.timeStamp("loading finished, waiting for deferred"); + } egw.window.console.groupEnd(); } @@ -702,6 +713,9 @@ export class etemplate2 { Promise.all(deferred).then(() => { + + console.timeEnd("deferred"); + console.timeStamp("Deferred done"); // Clear dirty now that it's all loaded this.widgetContainer.iterateOver((_widget) => {