From c3ea140b5a74783bc9f13ebc67a12608bbe064c5 Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 2 Mar 2023 13:27:35 -0700 Subject: [PATCH] Home: WIP home now shows stuff again --- api/js/etemplate/Et2Portlet/Et2Portlet.ts | 452 +++++++++++++++++++++ calendar/js/calendar_favorite_portlet.js | 4 +- calendar/js/et2_widget_timegrid.ts | 1 + home/inc/class.home_ui.inc.php | 2 +- home/js/Et2PortletFavorite.ts | 10 + home/js/{app.js => app.ts} | 456 +++++++++++----------- home/templates/default/app.css | 20 +- home/templates/default/index.xet | 15 +- home/templates/pixelegg/app.css | 11 - home/templates/pixelegg/app.less | 18 - 10 files changed, 722 insertions(+), 267 deletions(-) create mode 100644 api/js/etemplate/Et2Portlet/Et2Portlet.ts create mode 100644 home/js/Et2PortletFavorite.ts rename home/js/{app.js => app.ts} (65%) delete mode 100755 home/templates/pixelegg/app.css delete mode 100755 home/templates/pixelegg/app.less diff --git a/api/js/etemplate/Et2Portlet/Et2Portlet.ts b/api/js/etemplate/Et2Portlet/Et2Portlet.ts new file mode 100644 index 0000000000..1e8862da18 --- /dev/null +++ b/api/js/etemplate/Et2Portlet/Et2Portlet.ts @@ -0,0 +1,452 @@ +/** + * EGroupware eTemplate2 - Portlet base + * + * @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 + * @copyright 2022 Nathan Gray + */ + + +import {Et2Widget} from "../Et2Widget/Et2Widget"; +import {SlCard} from "@shoelace-style/shoelace"; +import interact from "@interactjs/interactjs"; +import {egw} from "../../jsapi/egw_global"; +import {classMap, css, html} from "@lion/core"; +import {HasSlotController} from "@shoelace-style/shoelace/dist/internal/slot"; +import shoelace from "../Styles/shoelace"; +import {Et2Dialog} from "../Et2Dialog/Et2Dialog"; +import {et2_IResizeable} from "../et2_core_interfaces"; +import {HomeApp} from "../../../../home/js/app"; + +/** + * Participate in Home + */ + +export class Et2Portlet extends (Et2Widget(SlCard)) +{ + static get properties() + { + return { + ...super.properties, + + + /** + * Give a title + * Goes in the header at the top with the icons + */ + title: {type: String}, + + /** + * Custom etemplate used to customize / set up the portlet + */ + editTemplate: {type: String}, + /** + * Set the portlet color + */ + color: {type: String}, + + /** + * Array of customization settings, similar in structure to preference settings + */ + settings: {type: Object}, + actions: {type: Object}, + } + } + + static get styles() + { + return [ + ...shoelace, + ...(super.styles || []), + css` + .portlet__header { + flex: 1 0 auto; + display: flex; + font-style: inherit; + font-variant: inherit; + font-weight: inherit; + font-stretch: inherit; + font-family: inherit; + font-size: var(--sl-font-size-medium); + line-height: var(--sl-line-height-dense); + padding: var(--header-spacing); + margin: 0px; + } + + .portlet__title { + flex: 1 1 auto; + font-size: var(--sl-font-size-medium); + user-select: none; + } + + .portlet__header et2-button-icon { + display: none; + order: 99; + margin-left: auto; + } + + .portlet__header:hover et2-button-icon { + display: initial; + } + + .card { + width: 100%; + height: 100% + } + + .card__body { + /* display block to prevent overflow from our size */ + display: block; + overflow: hidden; + flex: 1 1 auto; + } + + + ::slotted(div) { + } + ` + ] + } + + protected readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image'); + + /** + * These are the "normal" actions that every portlet is expected to have. + * The widget provides default actions for all of these, but they can + * be added to or overridden if needed by setting the action attribute. + */ + protected static default_actions : any = { + edit_settings: { + icon: "edit", + caption: "Configure", + "default": true, + hideOnDisabled: true, + group: "portlet" + }, + remove_portlet: { + icon: "delete", + caption: "Remove", + group: "portlet" + } + }; + + constructor() + { + super(); + this.editTemplate = egw.webserverUrl + "/home/templates/default/edit.xet" + this.actions = {}; + + this._onMoveResize = this._onMoveResize.bind(this); + this._onMoveResizeEnd = this._onMoveResizeEnd.bind(this); + } + + connectedCallback() + { + super.connectedCallback(); + + Promise.all([/* any others here...*/ this.updateComplete]) + .then(() => this._setupMoveResize()); + } + + /** + * Overriden from parent to add in default actions + */ + set_actions(actions) + { + // Set targets for actions + let defaults : any = {}; + for(let action_name in Et2Portlet.default_actions) + { + defaults[action_name] = Et2Portlet.default_actions[action_name]; + // Translate caption here, as translations aren't available earlier + defaults[action_name].caption = this.egw().lang(Et2Portlet.default_actions[action_name].caption); + if(typeof this[action_name] == "function") + { + defaults[action_name].onExecute = this[action_name].bind(this); + } + } + + // Add in defaults, but let provided actions override them + this.actions = jQuery.extend(true, {}, defaults, actions); + } + + /** + * Set up moving & resizing + */ + _setupMoveResize() + { + // Quick calculation of min size - dialog is made up of header, content & buttons + let minHeight = 0; + for(let e of this.children) + { + minHeight += e.getBoundingClientRect().height + parseFloat(getComputedStyle(e).marginTop) + parseFloat(getComputedStyle(e).marginBottom) + } + + // Get parent's dimensions + let style = getComputedStyle(this.parentElement); + let parentDimensions = { + width: parseInt(style.gridAutoColumns) + parseInt(style.gap) || HomeApp.GRID, + height: parseInt(style.gridAutoRows) + parseInt(style.gap) || HomeApp.GRID + }; + + let gridTarget = interact.snappers.grid({ + x: parentDimensions.width, + y: parentDimensions.height + }); + + interact(this) + .resizable({ + edges: {bottom: true, right: true}, + listeners: { + move: this._onMoveResize, + end: this._onMoveResizeEnd + }, + modifiers: [ + // Snap to grid + interact.modifiers.snap({ + targets: [gridTarget], + offset: "startCoords", + limits: {top: 0, left: 0} + }), + + // keep the edges inside the parent + interact.modifiers.restrictEdges({ + outer: 'parent' + }) + ] + }) + .draggable({ + allowFrom: ".portlet__header", + autoScroll: true, + listeners: { + move: this._onMoveResize, + end: this._onMoveResizeEnd + }, + modifiers: [ + // Restrict interferes with grid making it act strangely + //interact.modifiers.restrict({ + // restriction: 'parent' + //}), + // Snap to grid + interact.modifiers.snap({ + targets: [gridTarget], + offset: "startCoords", + limits: {top: 0, left: 0} + }) + ] + }); + } + + /** + * Handle moving and resizing + * + * @param event + */ + _onMoveResize(event : InteractEvent) + { + let target = event.target + let x = (parseFloat(target.getAttribute('data-x')) || 0) + (event.deltaRect ? 0 : event.dx); + let y = (parseFloat(target.getAttribute('data-y')) || 0) + (event.deltaRect ? 0 : event.dy); + + // update the element's style + // Size + target.style.width = event.rect.width + 'px' + target.style.height = event.rect.height + 'px' + + // Position + target.style.transform = 'translate(' + x + 'px,' + y + 'px)'; + + target.setAttribute('data-x', x); + target.setAttribute('data-y', y); + } + + /** + * Move or resize has completed. Now into parent grid and update settings. + * + * @param {InteractEvent} event + */ + _onMoveResizeEnd(event : InteractEvent) + { + + // Get parent's dimensions + let style = getComputedStyle(this.parentElement); + let parentDimensions = { + x: parseInt(style.gridAutoColumns) || 1, + y: parseInt(style.gridAutoRows) || 1 + } + let target = event.target + let dx = Math.round((parseInt(target.getAttribute('data-x')) || 0) / parentDimensions.x); + let dy = Math.round((parseInt(target.getAttribute('data-y')) || 0) / parentDimensions.y); + let dwidth = Math.round((event.deltaRect?.width || 1) / parentDimensions.x); + let dheight = Math.round((event.deltaRect?.height || 1) / parentDimensions.y); + let [o_x, o_width] = this.style.gridColumn.split(" / span"); + let [o_y, o_height] = this.style.gridRow.split(" / span"); + + // Clear temp stuff from moving + target.style.transform = ""; + target.style.width = ""; + target.style.height = ""; + target.removeAttribute('data-x'); + target.removeAttribute('data-y'); + + let col = Math.max(1, (dx + (parseInt(o_x) || 0))); + let row = Math.max(1, (dy + (parseInt(o_y) || 0))); + let width = (dwidth + parseInt(o_width)) || 1; + let height = (dheight + parseInt(o_height)) || 1; + + // Set grid position + target.style.gridArea = row + " / " + + col + "/ span " + + height + " / span " + + width; + + // Update position settings + this.update_settings({row: row, col: col, width: width, height: height}); + } + + + imageTemplate() + { + return ''; + } + + headerTemplate() + { + return html` +
+

${this.title}

+ +
`; + } + + footerTemplate() + { + return ''; + } + + + /** + * Create & show a dialog for customizing this portlet + * + * Properties for customization are sent in the 'settings' attribute + */ + edit_settings() + { + let dialog = new Et2Dialog(this.egw()); + dialog.transformAttributes({ + callback: this._process_edit.bind(this), + template: this.editTemplate, + value: { + content: this.settings + }, + buttons: Et2Dialog.BUTTONS_OK_CANCEL + }); + // Set separately to avoid translation + dialog.title = this.egw().lang("Edit") + " " + (this.title || ''); + this.appendChild(dialog); + } + + _process_edit(button_id, value) + { + if(button_id != Et2Dialog.OK_BUTTON) + { + return; + } + + // Pass updated settings, unless we're removing + this.update_settings((typeof value == 'string') ? {} : this.settings || {}); + + // Extend, not replace, because settings has types while value has just value + if(typeof value == 'object') + { + this.settings = {...this.settings, value}; + } + } + + protected update_settings(settings) + { + // Save settings - server might reply with new content if the portlet needs an update, + // but ideally it doesn't + this.classList.add("loading"); + + this.egw().jsonq("home.home_ui.ajax_set_properties", [this.id, [], settings, this.settings ? this.settings.group : false], + function(data) + { + // This section not for us + if(!data || typeof data.attributes == 'undefined') + { + return false; + } + + this.classList.remove("loading"); + + this.transformAttributes(data.attributes); + + // Flagged as needing to edit settings? Open dialog + if(typeof data.edit_settings != 'undefined' && data.edit_settings) + { + this.edit_settings(); + } + + // Only resize once, and only if needed + if(data.attributes.width || data.attributes.height) + { + this.style.columnSpan = data.attributes.width; + this.style.rowSpan = data.attributes.height; + + // Tell children + try + { + this.iterateOver(function(widget) + { + if(typeof widget.resize === 'function') + { + widget.resize(); + } + }, null, et2_IResizeable); + } + catch(e) + { + // Something went wrong, but do not stop + this.egw().debug('warn', e, this); + } + } + }, + this); + + } + + render() + { + return html` + +
+ ${this.imageTemplate()} + ${this.headerTemplate()} + + ${this.footerTemplate()} +
+ `; + } + +} + +if(!customElements.get("et2-portlet")) +{ + customElements.define("et2-portlet", Et2Portlet); +} \ No newline at end of file diff --git a/calendar/js/calendar_favorite_portlet.js b/calendar/js/calendar_favorite_portlet.js index 5889890f97..a0c139b181 100644 --- a/calendar/js/calendar_favorite_portlet.js +++ b/calendar/js/calendar_favorite_portlet.js @@ -17,7 +17,7 @@ * views, we need some custom handling to detect and handle refreshes. * * Note we put the class in home. - */ + * app.classes.home.calendar_favorite_portlet = app.classes.home.home_favorite_portlet.extend({ observer: function(_msg, _app, _id, _type, _msg_type, _targetapp) @@ -70,4 +70,4 @@ observer: function(_msg, _app, _id, _type, _msg_type, _targetapp) } } } -}); \ No newline at end of file +});*/ \ No newline at end of file diff --git a/calendar/js/et2_widget_timegrid.ts b/calendar/js/et2_widget_timegrid.ts index d90e24472c..59cb08c56c 100644 --- a/calendar/js/et2_widget_timegrid.ts +++ b/calendar/js/et2_widget_timegrid.ts @@ -30,6 +30,7 @@ import {EGW_AI_DRAG_ENTER, EGW_AI_DRAG_OUT} from "../../api/js/egw_action/egw_ac import {formatDate, formatTime, parseTime} from "../../api/js/etemplate/Et2Date/Et2Date"; import interact from "@interactjs/interactjs/index"; import type {InteractEvent} from "@interactjs/core/InteractEvent"; +import {CalendarApp} from "./app"; /** * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days diff --git a/home/inc/class.home_ui.inc.php b/home/inc/class.home_ui.inc.php index 724350ee36..fb32e05bf1 100644 --- a/home/inc/class.home_ui.inc.php +++ b/home/inc/class.home_ui.inc.php @@ -290,7 +290,7 @@ class home_ui $appname = $app; } } - Framework::includeJS('', $classname, $appname ? $appname : 'home'); + //Framework::includeJS('', $classname, $appname ? $appname : 'home'); if($full_exec) { diff --git a/home/js/Et2PortletFavorite.ts b/home/js/Et2PortletFavorite.ts new file mode 100644 index 0000000000..2b91ae5964 --- /dev/null +++ b/home/js/Et2PortletFavorite.ts @@ -0,0 +1,10 @@ +import {Et2Portlet} from "../../api/js/etemplate/Et2Portlet/Et2Portlet"; + +export class Et2PortletFavorite extends Et2Portlet +{ +} + +if(!customElements.get("et2-portlet-favorite")) +{ + customElements.define("et2-portlet-favorite", Et2PortletFavorite); +} \ No newline at end of file diff --git a/home/js/app.js b/home/js/app.ts similarity index 65% rename from home/js/app.js rename to home/js/app.ts index fdcd9fb83b..028c14884c 100644 --- a/home/js/app.js +++ b/home/js/app.ts @@ -8,17 +8,12 @@ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - /vendor/npm-asset/gridster/dist/jquery.gridster.js; -*/ import {AppJS} from "../../api/js/jsapi/app_base.js"; import {et2_createWidget} from "../../api/js/etemplate/et2_core_widget"; -import {et2_dialog} from "../../api/js/etemplate/et2_widget_dialog"; -import {et2_button} from "../../api/js/etemplate/et2_widget_button"; -// need legacy loading (uses this instead of window): import "../../vendor/npm-asset/gridster/dist/jquery.gridster.js"; -import "../../api/js/jsapi/egw_inheritance.js"; // Class +import {EgwApp} from "../../api/js/jsapi/egw_app"; +import {etemplate2} from "../../api/js/etemplate/etemplate2"; +import {Et2Portlet} from "../../api/js/etemplate/Et2Portlet/Et2Portlet"; +import {Et2PortletFavorite} from "./Et2PortletFavorite"; /** * JS for home application @@ -29,45 +24,40 @@ import "../../api/js/jsapi/egw_inheritance.js"; // Class * @see http://gridster.net * @augments AppJS */ -app.classes.home = (function(){ "use strict"; return AppJS.extend( +export class HomeApp extends EgwApp { - /** - * AppJS requires overwriting this with the actual application name - */ - appname: "home", /** * Grid resolution. Must match et2_portlet GRID */ - GRID: 50, + public static GRID = 50; /** * Default size for new portlets */ - DEFAULT: { - WIDTH: 4, - HEIGHT: 1 - }, + public static DEFAULT = { + WIDTH: 4, + HEIGHT: 1 + }; // List of portlets - portlets: {}, + private portlets = {}; + portlet_container : any; /** * Constructor * - * @memberOf app.home */ - init: function() + constructor() { // call parent - this._super.apply(this, arguments); - }, + super("home"); + } /** * Destructor - * @memberOf app.home */ - destroy: function() + destroy() { delete this.et2; delete this.portlet_container; @@ -75,15 +65,15 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( this.portlets = {}; // call parent - this._super.apply(this, arguments); + super.destroy(this.appname); // Make sure all other sub-etemplates in portlets are done - var others = etemplate2.getByApplication(this.appname); - for(var i = 0; i < others.length; i++) + let others = etemplate2.getByApplication(this.appname); + for(let i = 0; i < others.length; i++) { others[i].clear(); } - }, + } /** * This function is called when the etemplate2 object is loaded @@ -93,13 +83,13 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * @param {etemplate2} et2 Newly ready object * @param {string} Template name */ - et2_ready: function(et2, name) + et2_ready(et2, name) { // Top level if(name == 'home.index') { // call parent - this._super.apply(this, arguments); + super.et2_ready(et2, name); this.et2.set_id('home.index'); this.et2.set_actions(this.et2.getArrayMgr('modifications').getEntry('home.index')['actions']); @@ -107,22 +97,24 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( this.portlet_container = this.et2.getWidgetById("portlets"); // Set up sorting of portlets - this._do_ordering(); + //this._do_ordering(); // Accept drops of favorites, which aren't part of action system jQuery(this.et2.getDOMNode().parentNode).droppable({ hoverClass: 'drop-hover', - accept: function(draggable) { + accept: function(draggable) + { // Check for direct support for that application if(draggable[0].dataset && draggable[0].dataset.appname) { - return egw_getActionManager('home',false,1).getActionById('drop_'+draggable[0].dataset.appname +'_favorite_portlet') != null; + return egw_getActionManager('home', false, 1).getActionById('drop_' + draggable[0].dataset.appname + '_favorite_portlet') != null; } return false; }, - drop: function(event, ui) { + drop: function(event, ui) + { // Favorite dropped on home - fake an action and divert to normal handler - var action = { + let action = { data: { class: 'add_home_favorite_portlet' } @@ -131,38 +123,38 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( // Check for direct support for that application if(ui.helper.context.dataset && ui.helper.context.dataset.appname) { - var action = egw_getActionManager('home',false,1).getActionById('drop_'+ui.helper.context.dataset.appname +'_favorite_portlet') || {} + action = egw_getActionManager('home', false, 1).getActionById('drop_' + ui.helper.context.dataset.appname + '_favorite_portlet') || {} } action.ui = ui; app.home.add_from_drop(action, [{data: ui.helper.context.dataset}]) } }) - // Bind to unload to remove it from our list - .on('clear','.et2_container[id]', jQuery.proxy(function(e) { + // Bind to unload to remove it from our list + .on('clear', '.et2_container[id]', jQuery.proxy(function(e) + { if(e.target && e.target.id && this.portlets[e.target.id]) { this.portlets[e.target.id].destroy(); delete this.portlets[e.target.id]; } - },this)); + }, this)); } - else if (et2.uniqueId) + else if(et2.uniqueId) { let portlet_container = this.portlet_container || window.app.home?.portlet_container; // Handle bad timing - a sub-template was finished first if(!portlet_container) { - window.setTimeout(jQuery.proxy(function() {this.et2_ready(et2, name);},this),200); + window.setTimeout(() => {this.et2_ready(et2, name);}, 200); return; } - - var portlet = portlet_container.getWidgetById(et2.uniqueId); + let portlet = portlet_container.getWidgetById(et2.uniqueId); // Check for existing etemplate, this one loaded over it // NOTE: Moving them around like this can cause problems with event handlers - var existing = etemplate2.getById(et2.uniqueId); + let existing = etemplate2.getById(et2.uniqueId); if(portlet && existing) { - for(var i = 0; i < portlet._children.length; i++) + for(let i = 0; i < portlet._children.length; i++) { if(typeof portlet._children[i]._init == 'undefined') { @@ -170,11 +162,14 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( } } } + // Set size & position + let settings = portlet_container.getArrayMgr("content").data.find(e => e.id == et2.uniqueId) || {}; + portlet.style.gridArea = settings.row + "/" + settings.col + "/ span " + (settings.height || 1) + "/ span " + (settings.width || 1); + // It's in the right place for original load, but move it into portlet - var misplaced = jQuery(etemplate2.getById('home-index').DOMContainer).siblings('#'+et2.DOMContainer.id); + let misplaced = jQuery(etemplate2.getById('home-index').DOMContainer).siblings('#' + et2.DOMContainer.id); if(portlet) { - portlet.content = jQuery(et2.DOMContainer).appendTo(portlet.content); portlet.addChild(et2.widgetContainer); et2.resize(); } @@ -186,17 +181,7 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( // Instanciate custom code for this portlet this._get_portlet_code(portlet); } - - // Special handling to deal with legacy (non-et2) calendar links - if(name == 'home.legacy') - { - jQuery('.calendar_calDayColHeader a, .calendar_plannerDayScale a, .calendar_plannerWeekScale a, .calendar_plannerMonthScale a, .calendar_calGridHeader a', et2.DOMContainer) - .on('click', function(e) { - egw.link_handler(this.href,'calendar'); - return false; - }); - } - }, + } /** * Observer method receives update notifications from all applications @@ -217,9 +202,9 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * @param {string} _targetapp which app's window should be refreshed, default current * @return {false|*} false to stop regular refresh, thought all observers are run */ - observer: function(_msg, _app, _id, _type, _msg_type, _targetapp) + observer(_msg, _app, _id, _type, _msg_type, _targetapp) { - for(var id in this.portlets) + for(let id in this.portlets) { // App is home, refresh all portlets if(_app == 'home') @@ -231,68 +216,56 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( // Ask the portlets if they're interested try { - var code = this.portlets[id]; + let code = this.portlets[id]; if(code) { - code.observer(_msg,_app,_id,_type,_msg_type,_targetapp); + code.observer(_msg, _app, _id, _type, _msg_type, _targetapp); } } catch(e) { - this.egw.debug("error", "Error trying to update portlet " + id,e); + this.egw.debug("error", "Error trying to update portlet " + id, e); } } return false; - }, + } /** * Add a new portlet from the context menu */ - add: function(action, source) { + add(action, source) + { // Basic portlet attributes - var attrs = { - id: this._create_id(), - class: action.data.class, - width: this.DEFAULT.WIDTH, - height: this.DEFAULT.HEIGHT + let attrs = { + ...HomeApp.DEFAULT, ...{ + id: this._create_id(), + class: action.data.class + } }; // Try to put it about where the menu was opened if(action.menu_context) { - var $portlet_container = jQuery(this.portlet_container.getDOMNode()); - attrs.row = Math.max(1,Math.round((action.menu_context.posy - $portlet_container.offset().top )/ this.GRID)+1); - attrs.col = Math.max(1,Math.round((action.menu_context.posx - $portlet_container.offset().left) / this.GRID)+1); + let $portlet_container = jQuery(this.portlet_container.getDOMNode()); + attrs.row = Math.max(1, Math.round((action.menu_context.posy - $portlet_container.offset().top) / HomeApp.GRID) + 1); + attrs.col = Math.max(1, Math.round((action.menu_context.posx - $portlet_container.offset().left) / HomeApp.GRID) + 1); } - // Don't pass default width & height so class can set it - delete attrs.width; - delete attrs.height; - var portlet = et2_createWidget('portlet',jQuery.extend({},attrs), this.portlet_container); + let portlet = et2_createWidget('et2-portlet', attrs, this.portlet_container); portlet.loadingFinished(); - // Immediately add content ID so etemplate loads into the right place - portlet.content.append('
'); - // Get actual attributes & settings, since they're not available client side yet - portlet._process_edit(et2_dialog.OK_BUTTON, attrs); - - // Set up sorting/grid of new portlet - var $portlet_container = jQuery(this.portlet_container.getDOMNode()); - $portlet_container.data("gridster").add_widget( - portlet.getDOMNode(), - this.DEFAULT.WIDTH, this.DEFAULT.HEIGHT, - attrs.col, attrs.row - ); + portlet.update_settings(attrs); // Instanciate custom code for this portlet this._get_portlet_code(portlet); - }, + } /** * User dropped something on home. Add a new portlet */ - add_from_drop: function(action,source) { + add_from_drop(action, source) + { // Actions got confused drop vs popup if(source[0].id == 'portlets') @@ -300,10 +273,10 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( return this.add(action); } - var $portlet_container = jQuery(this.portlet_container.getDOMNode()); + let $portlet_container = jQuery(this.portlet_container.getDOMNode()); // Basic portlet attributes - var attrs = { + let attrs = { id: this._create_id(), class: action.data.class || action.id.substr(5), width: this.DEFAULT.WIDTH, @@ -313,18 +286,18 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( // Try to find where the drop was if(action != null && action.ui && action.ui.position) { - attrs.row = Math.max(1,Math.round((action.ui.position.top - $portlet_container.offset().top )/ this.GRID)); - attrs.col = Math.max(1,Math.round((action.ui.position.left - $portlet_container.offset().left) / this.GRID)); + attrs.row = Math.max(1, Math.round((action.ui.position.top - $portlet_container.offset().top) / this.GRID)); + attrs.col = Math.max(1, Math.round((action.ui.position.left - $portlet_container.offset().left) / this.GRID)); } - var portlet = et2_createWidget('portlet',jQuery.extend({},attrs), this.portlet_container); + let portlet = et2_createWidget('portlet', jQuery.extend({}, attrs), this.portlet_container); portlet.loadingFinished(); // Immediately add content ID so etemplate loads into the right place - portlet.content.append('
'); + portlet.content.append('
'); // Get actual attributes & settings, since they're not available client side yet - var drop_data = []; - for(var i = 0; i < source.length; i++) + let drop_data = []; + for(let i = 0; i < source.length; i++) { if(source[i].id) { @@ -338,7 +311,7 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( // Don't pass default width & height so class can set it delete attrs.width; delete attrs.height; - portlet._process_edit(et2_dialog.OK_BUTTON, jQuery.extend({dropped_data: drop_data},attrs)); + portlet._process_edit(et2_dialog.OK_BUTTON, jQuery.extend({dropped_data: drop_data}, attrs)); // Set up sorting/grid of new portlet $portlet_container.data("gridster").add_widget( @@ -349,7 +322,7 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( // Instanciate custom code for this portlet this._get_portlet_code(portlet); - }, + } /** * Set the current selection as default for other users @@ -360,25 +333,27 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * @param {egwAction} action * @param {egwActionObject[]} selected */ - set_default: function(action, selected) { + set_default(action, selected) + { // Gather just IDs, server will handle the details - var portlet_ids = []; - var group = action.data.portlet_group || false; + let portlet_ids = []; + let group = action.data.portlet_group || false; if(selected[0].id == 'home.index') { // Set all - this.portlet_container.iterateOver(function(portlet) { + this.portlet_container.iterateOver(function(portlet) + { portlet_ids.push(portlet.id); - },this,et2_portlet); + }, this, et2_portlet); } else { - for(var i = 0; i < selected.length; i++) + for(let i = 0; i < selected.length; i++) { portlet_ids.push(selected[i].id); // Read the associated group so we can properly remove it - var portlet = egw.preference(selected[i].id,'home'); + let portlet = egw.preference(selected[i].id, 'home'); if(!group && portlet && portlet.group) { group = portlet.group; @@ -395,33 +370,38 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( egw.json('home_ui::ajax_set_default', ['delete', portlet_ids, group]).sendRequest(true); return; } - var dialog = et2_createWidget("dialog",{ + let dialog = et2_createWidget("dialog", { // If you use a template, the second parameter will be the value of the template, as if it were submitted. - callback: function(button_id, value) { - if(button_id != et2_dialog.OK_BUTTON) return; + callback: function(button_id, value) + { + if(button_id != et2_dialog.OK_BUTTON) + { + return; + } // Pass them to server - egw.json('home_ui::ajax_set_default', ['add', portlet_ids, value.group||false]).sendRequest(true); + egw.json('home_ui::ajax_set_default', ['add', portlet_ids, value.group || false]).sendRequest(true); }, buttons: et2_dialog.BUTTONS_OK_CANCEL, title: action.caption, - template:"home.set_default", - value: {content:{}, sel_options: {group:{default: egw.lang('All'), forced: egw.lang('Forced')}}} + template: "home.set_default", + value: {content: {}, sel_options: {group: {default: egw.lang('All'), forced: egw.lang('Forced')}}} }); - }, + } /** * Allow a refresh from anywhere by triggering an update with no changes * * @param {string} id */ - refresh: function(id) { - var p = this.portlet_container.getWidgetById(id); + refresh(id) + { + let p = this.portlet_container.getWidgetById(id); if(p) { p._process_edit(et2_dialog.OK_BUTTON, '~reload~'); } - }, + } /** * Determine the best fitting code to use for the given portlet, instanciate @@ -430,33 +410,36 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * @param {et2_portlet} portlet * @returns {home_portlet} */ - _get_portlet_code: function(portlet) { - var classname = portlet.class; + _get_portlet_code(portlet) + { + let classname = portlet.class; // Freshly added portlets can have 'add_' prefix if(classname.indexOf('add_') == 0) { - classname = classname.replace('add_',''); + classname = classname.replace('add_', ''); } // Prefer a specific match - var _class = app.classes.home[classname] || + let _class = app.classes.home[classname] || + (typeof customElements.get(classname) != "undefined" ? customElements.get(classname).class : false) || // If it has a nextmatch, use favorite base class - (portlet.getWidgetById('nm') ? app.classes.home.home_favorite_portlet : false) || + (portlet.getWidgetById('nm') ? Et2PortletFavorite : false) || // Fall back to base class - app.classes.home.home_portlet; + Et2Portlet; this.portlets[portlet.id] = new _class(portlet); return this.portlets[portlet.id]; - }, + } /** * For link_portlet - opens the configured record when the user * double-clicks or chooses view from the context menu */ - open_link: function(action) { + open_link(action) + { // Get widget - var widget = null; + let widget = null; while(action.parent != null) { if(action.data && action.data.widget) @@ -468,26 +451,25 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( } if(widget == null) { - egw().log("warning", "Could not find widget"); + this.egw.log("warning", "Could not find widget"); return; } - egw().open(widget.options.settings.entry, "", 'view',null,widget.options.settings.entry.app); - }, + this.egw.open(widget.options.settings.entry, "", 'view', null, widget.options.settings.entry.app); + } /** * Set up the drag / drop / re-order of portlets */ - _do_ordering: function() { - var $portlet_container = jQuery(this.portlet_container.getDOMNode()); + _do_ordering() + { + let $portlet_container = jQuery(this.portlet_container.getDOMNode()); $portlet_container - .addClass("home ui-helper-clearfix") - .disableSelection() /* Gridster */ .gridster({ widget_selector: 'div.et2_portlet', // Dimensions + margins = grid spacing - widget_base_dimensions: [this.GRID-5, this.GRID-5], - widget_margins: [5,5], + widget_base_dimensions: [home.GRID - 5, home.GRID - 5], + widget_margins: [5, 5], extra_rows: 1, extra_cols: 1, min_cols: 3, @@ -498,9 +480,10 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * @param grid Object Grid settings * @return Object - will be returned by gridster.serialize() */ - serialize_params: function($w, grid) { + serialize_params: function($w, grid) + { return { - id: $w.attr('id').replace(app.home.portlet_container.getInstanceManager().uniqueId+'_',''), + id: $w.attr('id').replace(app.home.portlet_container.getInstanceManager().uniqueId + '_', ''), row: grid.row, col: grid.col, width: grid.size_x, @@ -512,24 +495,31 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( */ draggable: { handle: '.ui-widget-header', - stop: function(event,ui) { + stop: function(event, ui) + { // Update widget(s) - var changed = this.serialize_changed(); + let changed = this.serialize_changed(); // Reset changed, or they keep accumulating this.$changed = jQuery([]); - for (var key in changed) + for(let key in changed) { - if(!changed[key].id) continue; + if(!changed[key].id) + { + continue; + } // Changed ID is the ID - var widget = window.app.home.portlet_container.getWidgetById(changed[key].id); - if(!widget || widget == window.app.home.portlet_container) continue; + let widget = window.app.home.portlet_container.getWidgetById(changed[key].id); + if(!widget || widget == window.app.home.portlet_container) + { + continue; + } - egw().jsonq("home.home_ui.ajax_set_properties",[changed[key].id, {},{ + egw().jsonq("home.home_ui.ajax_set_properties", [changed[key].id, {}, { row: changed[key].row, col: changed[key].col - },widget.settings?widget.settings.group:false], + }, widget.settings ? widget.settings.group : false], null, widget, true, widget ); @@ -540,52 +530,57 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( }); // Rescue selectboxes from Firefox - $portlet_container.on('mousedown touchstart', 'select', function(e) { + $portlet_container.on('mousedown touchstart', 'select', function(e) + { e.stopPropagation(); }); // Bind window resize to re-layout gridster - jQuery(window).one("resize."+this.et2._inst.uniqueId, function() { + jQuery(window).one("resize." + this.et2._inst.uniqueId, function() + { // Note this doesn't change the positions, just makes them invalid $portlet_container.data('gridster').recalculate_faux_grid(); }); // Bind resize to update gridster - this may happen _before_ the widget gets a // chance to update itself, so we can't use the widget $portlet_container - .on("resizestop", function(event, ui) { + .on("resizestop", function(event, ui) + { $portlet_container.data("gridster").resize_widget( ui.element, Math.round(ui.size.width / app.home.GRID), Math.round(ui.size.height / app.home.GRID) ); }); - }, + } /** * Create an ID that should be unique, at least amoung a single user's portlets */ - _create_id: function() { - var id = ''; + _create_id() + { + let id = ''; do { id = Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); + .toString(16) + .substring(1); } - while(this.portlet_container.getWidgetById('portlet_'+id)); - return 'portlet_'+id; - }, + while(this.portlet_container.getWidgetById('portlet_' + id)); + return 'portlet_' + id; + } /** * Functions for the list portlet */ /** - * For list_portlet - opens a dialog to add a new entry to the list - * - * @param {egwAction} action Drop or add action - * @param {egwActionObject[]} Selected entries - * @param {egwActionObject} target_action Drop target - */ - add_link: function(action, source, target_action) { + * For list_portlet - opens a dialog to add a new entry to the list + * + * @param {egwAction} action Drop or add action + * @param {egwActionObject[]} Selected entries + * @param {egwActionObject} target_action Drop target + */ + add_link(action, source, target_action) + { // Actions got confused drop vs popup if(source[0].id == 'portlets') { @@ -593,7 +588,7 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( } // Get widget - var widget = null; + let widget = null; while(action.parent != null) { if(action.data && action.data.widget) @@ -606,14 +601,18 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( if(target_action == null) { // use template base url from initial template, to continue using webdav, if that was loaded via webdav - var splitted = 'home.edit'.split('.'); - var path = app.home.portlet_container.getRoot()._inst.template_base_url + splitted.shift() + "/templates/default/" + - splitted.join('.')+ ".xet"; - var dialog = et2_createWidget("dialog",{ - callback: function(button_id, value) { - if(button_id == et2_dialog.CANCEL_BUTTON) return; - var new_list = widget.options.settings.list || []; - for(var i = 0; i < new_list.length; i++) + let splitted = 'home.edit'.split('.'); + let path = app.home.portlet_container.getRoot()._inst.template_base_url + splitted.shift() + "/templates/default/" + + splitted.join('.') + ".xet"; + let dialog = et2_createWidget("dialog", { + callback: function(button_id, value) + { + if(button_id == et2_dialog.CANCEL_BUTTON) + { + return; + } + let new_list = widget.options.settings.list || []; + for(let i = 0; i < new_list.length; i++) { if(new_list[i].app == value.add.app && new_list[i].id == value.add.id) { @@ -624,9 +623,9 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( value.add.link_id = value.add.app + ':' + value.add.id; // Update server side new_list.push(value.add); - widget._process_edit(button_id,{list: new_list}); + widget._process_edit(button_id, {list: new_list}); // Update client side - var list = widget.getWidgetById('list'); + let list = widget.getWidgetById('list'); if(list) { list.set_value(new_list); @@ -634,41 +633,41 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( }, buttons: et2_dialog.BUTTONS_OK_CANCEL, title: app.home.egw.lang('add'), - template:path, - value: { content: [{label: app.home.egw.lang('add'),type: 'link-entry',name: 'add',size:''}]} + template: path, + value: {content: [{label: app.home.egw.lang('add'), type: 'link-entry', name: 'add', size: ''}]} }); } else { // Drag'n'dropped something on the list - just send action IDs - var new_list = widget.options.settings.list || []; - var changed = false; - for(var i = 0; i < new_list.length; i++) + let new_list = widget.options.settings.list || []; + let changed = false; + for(let i = 0; i < new_list.length; i++) { // Avoid duplicates - for(var j = 0; j < source.length; j++) + for(let j = 0; j < source.length; j++) { - if(!source[j].id || new_list[i].app+"::"+new_list[i].id == source[j].id) + if(!source[j].id || new_list[i].app + "::" + new_list[i].id == source[j].id) { // Duplicate - skip it - source.splice(j,1); + source.splice(j, 1); } } } - for(var i = 0; i < source.length; i++) + for(let i = 0; i < source.length; i++) { - var explode = source[i].id.split('::'); - new_list.push({app: explode[0],id: explode[1], link_id: explode.join(':')}); + let explode = source[i].id.split('::'); + new_list.push({app: explode[0], id: explode[1], link_id: explode.join(':')}); changed = true; } if(changed) { - widget._process_edit(et2_dialog.OK_BUTTON,{ + widget._process_edit(et2_dialog.OK_BUTTON, { list: new_list || {} }); } // Filemanager support - links need app = 'file' and type set - for(var i = 0; i < new_list.length; i++) + for(let i = 0; i < new_list.length; i++) { if(new_list[i]['app'] == 'filemanager') { @@ -679,20 +678,21 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( widget.getWidgetById('list').set_value(new_list); } - }, + } /** * Remove a link from the list */ - link_change: function(list, link_id, row) { + link_change(list, link_id, row) + { // Quick response client side row.slideUp(row.remove); // Actual removal - var portlet = list._parent._parent; + let portlet = list._parent._parent; portlet.options.settings.list.splice(row.index(), 1); - portlet._process_edit(et2_dialog.OK_BUTTON,{list: portlet.options.settings.list || {}}); - }, + portlet._process_edit(et2_dialog.OK_BUTTON, {list: portlet.options.settings.list || {}}); + } /** * Functions for the note portlet @@ -704,33 +704,34 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * @param {egwAction} action * @param {egwActionObject[]} Selected */ - note_edit: function(action, selected) { + note_edit(action, selected) + { if(!selected && typeof action == 'string') { - var id = action; + let id = action; } else { - var id = selected[0].id; + let id = selected[0].id; } // Aim to match the size - var portlet_dom = jQuery('[id$='+id+'][data-sizex]',this.portlet_container.getDOMNode()); - var width = portlet_dom.attr('data-sizex') * this.GRID; - var height = portlet_dom.attr('data-sizey') * this.GRID; + let portlet_dom = jQuery('[id$=' + id + '][data-sizex]', this.portlet_container.getDOMNode()); + let width = portlet_dom.attr('data-sizex') * this.GRID; + let height = portlet_dom.attr('data-sizey') * this.GRID; // CKEditor is impossible to use below a certain size // Add 35px for the toolbar, 35px for the buttons - var window_width = Math.max(580, width+20); - var window_height = Math.max(350, height+70); + let window_width = Math.max(580, width + 20); + let window_height = Math.max(350, height + 70); // Open popup, but add 70 to the height for the toolbar - egw.open_link(egw.link('/index.php',{ + this.egw.open_link(this.egw.link('/index.php', { menuaction: 'home.home_note_portlet.edit', id: id, height: window_height - 70 - }),'home_'+id, window_width+'x'+window_height,'home'); - }, + }), 'home_' + id, window_width + 'x' + window_height, 'home'); + } /** * Favorites / nextmatch @@ -741,18 +742,24 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * @param {Event} event * @param {et2_button} widget */ - nextmatch_toggle_header: function(event, widget) { + nextmatch_toggle_header(event, widget) + { widget.set_class(widget.class == 'opened' ? 'closed' : 'opened'); // We operate on the DOM here, nm should be unaware of our fiddling - var nm = widget.getParent().getWidgetById('nm'); - if(!nm) return; + let nm = widget.getParent().getWidgetById('nm'); + if(!nm) + { + return; + } // Hide header nm.div.toggleClass('header_hidden'); nm.set_hide_header(nm.div.hasClass('header_hidden')); nm.resize(); } -})}).call(window); +} + +app.classes.home = HomeApp; /// Base class code @@ -763,15 +770,19 @@ app.classes.home = (function(){ "use strict"; return AppJS.extend( * * @type @exp;Class@call;extend */ -app.classes.home.home_portlet = Class.extend({ - portlet: null, +export class HomePortlet +{ + protected portlet = null; - init: function(portlet) { + init(portlet) + { this.portlet = portlet; - }, - destroy: function() { + } + + destroy() + { this.portlet = null; - }, + } /** * Handle framework refresh messages to determine if the portlet needs to @@ -780,12 +791,13 @@ app.classes.home.home_portlet = Class.extend({ * App is responsible for only reacting to "messages" it is interested in! * */ - observer: function(_msg, _app, _id, _type, _msg_type, _targetapp) + observer(_msg, _app, _id, _type, _msg_type, _targetapp) { // Not interested } -}); +} +/* app.classes.home.home_link_portlet = app.classes.home.home_portlet.extend({ init: function(portlet) { // call parent @@ -794,7 +806,7 @@ app.classes.home.home_link_portlet = app.classes.home.home_portlet.extend({ // Check for tooltip if(this.portlet) { - var content = jQuery('.tooltip',this.portlet.content); + let content = jQuery('.tooltip', this.portlet.content); if(content.length && content.children().length) { //Check if the tooltip is already initialized @@ -831,7 +843,7 @@ app.classes.home.home_link_portlet = app.classes.home.home_portlet.extend({ { if(this.portlet && this.portlet.settings) { - var value = this.portlet.settings.entry || {}; + let value = this.portlet.settings.entry || {}; if(value.app && value.app == _app && value.id && value.id == _id) { // We don't just get the updated title, in case there's a custom @@ -846,8 +858,8 @@ app.classes.home.home_list_portlet = app.classes.home.home_portlet.extend({ { if(this.portlet && this.portlet.getWidgetById('list')) { - var list = this.portlet.getWidgetById('list').options.value; - for(var i = 0; i < list.length; i++) + let list = this.portlet.getWidgetById('list').options.value; + for(let i = 0; i < list.length; i++) { if(list[i].app == _app && list[i].id == _id) { diff --git a/home/templates/default/app.css b/home/templates/default/app.css index df4198e1aa..bd1ad0ff6b 100644 --- a/home/templates/default/app.css +++ b/home/templates/default/app.css @@ -10,6 +10,16 @@ } #home-index_portlets { background-color: inherit; + display: grid; + grid-auto-columns: 50ex; + grid-auto-rows: 50ex; + + gap: 2ex; + + justify-content: stretch; + align-content: stretch; + justify-items: stretch; + align-items: stretch; } #portlets { border: 1px solid silver; @@ -26,14 +36,8 @@ .et2_portlet.ui-widget-content > div { } -.et2_portlet.ui-widget-content > div:last-of-type { - /* Allow space for header, as the whole portlet is sized by auto-generated css */ - position: absolute; - bottom: 0px; - top: 20px; - width: 100%; - overflow: hidden; -} + + .et2_portlet .et2_container { height: 100%; } diff --git a/home/templates/default/index.xet b/home/templates/default/index.xet index 54d65f7fac..b4341be2de 100644 --- a/home/templates/default/index.xet +++ b/home/templates/default/index.xet @@ -17,11 +17,16 @@ - - - - - + + + + + + + diff --git a/home/templates/pixelegg/app.css b/home/templates/pixelegg/app.css deleted file mode 100755 index c012d241d6..0000000000 --- a/home/templates/pixelegg/app.css +++ /dev/null @@ -1,11 +0,0 @@ -/** - * EGroupware: CSS with less preprocessor - * - * Please do NOT change app.css directly, instead change app.less and compile it! - * - * @link http://www.egroupware.org - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @author Stefan Reinhardt - * @package home - * @version $Id$ - */ diff --git a/home/templates/pixelegg/app.less b/home/templates/pixelegg/app.less deleted file mode 100755 index 7e4d0aff38..0000000000 --- a/home/templates/pixelegg/app.less +++ /dev/null @@ -1,18 +0,0 @@ -/** - * EGroupware: CSS with less preprocessor - * - * Please do NOT change app.css directly, instead change app.less and compile it! - * - * @link http://www.egroupware.org - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @author Stefan Reinhardt - * @package home - * @version $Id$ - */ - -@import (reference) "../../../pixelegg/less/def_buttons.less"; -@import (reference) "../../../pixelegg/less/def_design_pattern_color_font_shadow.less"; - -@import (reference) "../default/app.css"; - -