From d37143c8424f25c20655c8937f8dfe52ab48666d Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 3 Mar 2023 15:41:56 -0700 Subject: [PATCH] Home WIP Favorites working a little better --- api/js/etemplate/Et2Portlet/Et2Portlet.ts | 101 ++++-- api/js/etemplate/et2_widget_box.ts | 2 +- home/inc/class.home_favorite_portlet.inc.php | 47 ++- home/inc/class.home_list_portlet.inc.php | 15 +- home/inc/class.home_portlet.inc.php | 30 +- home/inc/class.home_ui.inc.php | 11 +- home/inc/class.home_weather_portlet.inc.php | 2 +- home/js/Et2PortletFavorite.ts | 51 +++ home/js/Et2PortletList.ts | 175 +++++++++++ home/js/app.ts | 309 ++++--------------- home/templates/default/app.css | 5 +- home/templates/default/favorite.xet | 1 - home/templates/default/index.xet | 31 +- 13 files changed, 478 insertions(+), 302 deletions(-) create mode 100644 home/js/Et2PortletList.ts diff --git a/api/js/etemplate/Et2Portlet/Et2Portlet.ts b/api/js/etemplate/Et2Portlet/Et2Portlet.ts index 1e8862da18..8f30c2cb3f 100644 --- a/api/js/etemplate/Et2Portlet/Et2Portlet.ts +++ b/api/js/etemplate/Et2Portlet/Et2Portlet.ts @@ -13,8 +13,9 @@ import {Et2Widget} from "../Et2Widget/Et2Widget"; import {SlCard} from "@shoelace-style/shoelace"; import interact from "@interactjs/interactjs"; +import type {InteractEvent} from "@interactjs/core/InteractEvent"; import {egw} from "../../jsapi/egw_global"; -import {classMap, css, html} from "@lion/core"; +import {classMap, css, html, TemplateResult} from "@lion/core"; import {HasSlotController} from "@shoelace-style/shoelace/dist/internal/slot"; import shoelace from "../Styles/shoelace"; import {Et2Dialog} from "../Et2Dialog/Et2Dialog"; @@ -25,7 +26,7 @@ import {HomeApp} from "../../../../home/js/app"; * Participate in Home */ -export class Et2Portlet extends (Et2Widget(SlCard)) +export class Et2Portlet extends Et2Widget(SlCard) { static get properties() { @@ -62,8 +63,12 @@ export class Et2Portlet extends (Et2Widget(SlCard)) ...shoelace, ...(super.styles || []), css` + :host { + --header-spacing: var(--sl-spacing-medium); + } + .portlet__header { - flex: 1 0 auto; + flex: 0 0 auto; display: flex; font-style: inherit; font-variant: inherit; @@ -73,7 +78,9 @@ export class Et2Portlet extends (Et2Widget(SlCard)) font-size: var(--sl-font-size-medium); line-height: var(--sl-line-height-dense); padding: var(--header-spacing); + padding-right: calc(2em + var(--header-spacing)); margin: 0px; + position: relative; } .portlet__title { @@ -84,8 +91,8 @@ export class Et2Portlet extends (Et2Widget(SlCard)) .portlet__header et2-button-icon { display: none; - order: 99; - margin-left: auto; + position: absolute; + right: 0px; } .portlet__header:hover et2-button-icon { @@ -97,11 +104,17 @@ export class Et2Portlet extends (Et2Widget(SlCard)) height: 100% } + .card_header { + margin-right: calc(var(--sl-spacing-medium) + 1em); + } + .card__body { /* display block to prevent overflow from our size */ display: block; overflow: hidden; + flex: 1 1 auto; + padding: 0px; } @@ -151,6 +164,19 @@ export class Et2Portlet extends (Et2Widget(SlCard)) .then(() => this._setupMoveResize()); } + /** + * Load further details from content + * + * Normal load & attribute assign will cast our settings object to a string + * @param _template_node + */ + transformAttributes(attrs) + { + super.transformAttributes(attrs); + let data = this.getArrayMgr("content").data.find(e => e.id && e.id == this.id); + this.settings = typeof attrs.settings == "string" ? data.value || data.settings || {} : attrs.settings; + } + /** * Overriden from parent to add in default actions */ @@ -316,16 +342,17 @@ export class Et2Portlet extends (Et2Widget(SlCard)) headerTemplate() { return html` -
-

${this.title}

- -
`; +

${this.title}

`; } - footerTemplate() + bodyTemplate() : TemplateResult { - return ''; + return html``; + } + + footerTemplate() : TemplateResult + { + return html``; } @@ -343,7 +370,27 @@ export class Et2Portlet extends (Et2Widget(SlCard)) value: { content: this.settings }, - buttons: Et2Dialog.BUTTONS_OK_CANCEL + buttons: [ + { + "button_id": Et2Dialog.OK_BUTTON, + label: this.egw().lang('ok'), + id: 'dialog[ok]', + image: 'check', + "default": true + }, + { + label: this.egw().lang('delete'), + id: 'delete', + image: 'delete', + align: "right" + }, + { + "button_id": Et2Dialog.CANCEL_BUTTON, + label: this.egw().lang('cancel'), + id: 'cancel', + image: 'cancel' + } + ], }); // Set separately to avoid translation dialog.title = this.egw().lang("Edit") + " " + (this.title || ''); @@ -354,11 +401,18 @@ export class Et2Portlet extends (Et2Widget(SlCard)) { if(button_id != Et2Dialog.OK_BUTTON) { + if(button_id == "delete") + { + this.update_settings('~remove~').then(() => + { + this.remove(); + }); + } return; } // Pass updated settings, unless we're removing - this.update_settings((typeof value == 'string') ? {} : this.settings || {}); + this.update_settings({...this.settings, ...value}); // Extend, not replace, because settings has types while value has just value if(typeof value == 'object') @@ -367,13 +421,19 @@ export class Et2Portlet extends (Et2Widget(SlCard)) } } - protected update_settings(settings) + public update_settings(settings) { + // Skip any updates during loading + if(!this.getInstanceManager().isReady) + { + return; + } + // 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], + return 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 @@ -437,8 +497,13 @@ export class Et2Portlet extends (Et2Widget(SlCard)) })} > ${this.imageTemplate()} - ${this.headerTemplate()} - + +
+ ${this.headerTemplate()} + +
+ ${this.bodyTemplate()} ${this.footerTemplate()} `; diff --git a/api/js/etemplate/et2_widget_box.ts b/api/js/etemplate/et2_widget_box.ts index 17421ad3cc..7255cbbbcc 100644 --- a/api/js/etemplate/et2_widget_box.ts +++ b/api/js/etemplate/et2_widget_box.ts @@ -94,7 +94,7 @@ export class et2_box extends et2_baseWidget implements et2_IDetachedDOM // Create the new element, if no expansion needed var id = et2_readAttrWithDefault(node, "id", ""); - if(id.indexOf('$') < 0 || ['box', 'grid', 'et2-box'].indexOf(widgetType) == -1) + if(id.indexOf('$') < 0 || ['box', 'grid', 'et2-box'].indexOf(widgetType) == -1 && typeof customElements.get(widgetType) == "undefined") { this.createElementFromNode(node); childIndex++; diff --git a/home/inc/class.home_favorite_portlet.inc.php b/home/inc/class.home_favorite_portlet.inc.php index 7797e973cc..02b8cde220 100644 --- a/home/inc/class.home_favorite_portlet.inc.php +++ b/home/inc/class.home_favorite_portlet.inc.php @@ -145,16 +145,57 @@ class home_favorite_portlet extends home_portlet public static function process($content = array()) { - unset($content); // not used, but required by function signature + unset($content); // not used, but required by function signature // We need to keep the template going, thanks. - Etemplate\Widget::setElementAttribute('','',''); + Etemplate\Widget::setElementAttribute('', '', ''); } - public function get_actions(){ + public function get_actions() + { return array(); } + public function get_type() + { + return 'et2-portlet-favorite'; + } + + /** + * Get a list of "Add" actions + * @return array + */ + public function get_add_actions() + { + $desc = $this->get_description(); + $actions = array(); + + // Add a list of favorites + if($this->context['appname'] && ($favorites = Framework\Favorites::get_favorites($this->context['appname']))) + { + foreach($favorites as $name => $favorite) + { + $actions[] = array( + 'id' => __CLASS__ . $name, + 'caption' => $name, + 'onExecute' => 'javaScript:app.home.add' + ); + } + } + else + { + $actions[] = array( + 'id' => __CLASS__, + 'caption' => lang('List'), + 'hint' => $desc['description'], + 'onExecute' => 'javaScript:app.home.add', + 'acceptedTypes' => $this->accept_drop(), + 'allowOnMultiple' => $this->accept_multiple() + ); + } + return $actions; + } + /** * Some descriptive information about the portlet, so that users can decide if * they want it or not, and for inclusion in lists, hover text, etc. diff --git a/home/inc/class.home_list_portlet.inc.php b/home/inc/class.home_list_portlet.inc.php index 4ef571521e..13a432f298 100644 --- a/home/inc/class.home_list_portlet.inc.php +++ b/home/inc/class.home_list_portlet.inc.php @@ -92,17 +92,22 @@ class home_list_portlet extends home_portlet public function get_description() { return array( - 'displayName'=> 'List of entries', - 'title'=> $this->title, - 'description'=> lang('Show a list of entries') + 'displayName' => 'List of entries', + 'title' => $this->title, + 'description' => lang('Show a list of entries') ); } + public function get_type() + { + return 'et2-portlet-list'; + } + /** * Get a fragment of HTML for display * * @param id String unique ID, provided to the portlet so it can make sure content is - * unique, if needed. + * unique, if needed. * @return string HTML fragment for display */ public function exec($id = null, Etemplate &$etemplate = null) @@ -127,7 +132,7 @@ class home_list_portlet extends home_portlet } } - $etemplate->exec('home.home_list_portlet.exec',$content); + //$etemplate->exec('home.home_list_portlet.exec',$content); } /** diff --git a/home/inc/class.home_portlet.inc.php b/home/inc/class.home_portlet.inc.php index 2de595fc7b..cdcc980f1d 100644 --- a/home/inc/class.home_portlet.inc.php +++ b/home/inc/class.home_portlet.inc.php @@ -49,11 +49,39 @@ abstract class home_portlet */ public abstract function get_description(); + /** + * Get the web-component tag used client side + * + * @return string + */ + public function get_type() + { + return 'et2-portlet'; + } + + /** + * Get a list of "Add" actions + * @return array + */ + public function get_add_actions() + { + $desc = $this->get_description(); + return array( + array( + 'id' => __CLASS__, + 'caption' => $desc['displayName'], + 'hint' => $desc['description'], + 'onExecute' => 'javaScript:app.home.add', + 'acceptedTypes' => $this->accept_drop(), + 'allowOnMultiple' => $this->accept_multiple() + )); + } + /** * Generate the display for the portlet * * @param id String unique ID, provided to the portlet so it can make sure content is - * unique, if needed. + * unique, if needed. * @param Etemplate $etemplate eTemplate to generate content */ public abstract function exec($id = null, Etemplate &$etemplate = null); diff --git a/home/inc/class.home_ui.inc.php b/home/inc/class.home_ui.inc.php index fb32e05bf1..d213647bde 100644 --- a/home/inc/class.home_ui.inc.php +++ b/home/inc/class.home_ui.inc.php @@ -115,8 +115,7 @@ class home_ui 'onExecute' => 'javaScript:app.home.add', 'children' => $add_portlets ), - // Favorites are sortable which needs special handling, - // handled directly through jQuery + // Favorites are sortable which needs special handling ); // Add all known portlets as drop actions too. If there are multiple matches, there will be a menu @@ -180,8 +179,9 @@ class home_ui $portlet = new $classname($context); $desc = $portlet->get_description(); $portlet_content = array( - 'id' => $id - ) + $desc + $context; + 'id' => $id, + 'type' => $portlet->get_type() + ) + $desc + $context; // Get settings @@ -566,6 +566,7 @@ class home_ui else { $prefs->delete('home', $portlet_id); + $response->data([]); } } else @@ -594,7 +595,7 @@ class home_ui $classname = substr($classname, 4); } $content = null; - $portlet = $this->get_portlet($portlet_id, $context, $content, $attributes, $full_exec); + $portlet = $this->get_portlet("home-index_portlets_" . $portlet_id, $context, $content, $attributes, $full_exec); $context['class'] = get_class($portlet); foreach($portlet->get_properties() as $property) diff --git a/home/inc/class.home_weather_portlet.inc.php b/home/inc/class.home_weather_portlet.inc.php index fc858f854c..8a9763d8f5 100644 --- a/home/inc/class.home_weather_portlet.inc.php +++ b/home/inc/class.home_weather_portlet.inc.php @@ -39,7 +39,7 @@ class home_weather_portlet extends home_portlet if (false) parent::__construct(); // City not set for new widgets created via context menu - if(!$context['city'] || $context['height'] < 2) + if(!$context['city']) { // Set initial size to 3x2, default is too small $context['width'] = 3; diff --git a/home/js/Et2PortletFavorite.ts b/home/js/Et2PortletFavorite.ts index 2b91ae5964..a8c6cda0d0 100644 --- a/home/js/Et2PortletFavorite.ts +++ b/home/js/Et2PortletFavorite.ts @@ -1,7 +1,58 @@ import {Et2Portlet} from "../../api/js/etemplate/Et2Portlet/Et2Portlet"; +import {css, html} from "@lion/core"; +import shoelace from "../../api/js/etemplate/Styles/shoelace"; +import {etemplate2} from "../../api/js/etemplate/etemplate2"; export class Et2PortletFavorite extends Et2Portlet { + static get styles() + { + return [ + ...shoelace, + ...(super.styles || []), + css` + .portlet__header et2-button { + visibility: hidden; + } + + .portlet__header:hover et2-button { + visibility: visible; + } + ` + ] + } + + constructor() + { + super(); + this.toggleHeader = this.toggleHeader.bind(this); + } + + headerTemplate() + { + return html`${super.headerTemplate()} + + `; + } + + public toggleHeader() + { + //widget.set_class(widget.class == 'opened' ? 'closed' : 'opened'); + // We operate on the DOM here, nm should be unaware of our fiddling + let nm = this.getWidgetById('nm') || etemplate2.getById(this.id) && etemplate2.getById(this.id).widgetContainer.getWidgetById('nm') || false; + if(!nm) + { + return; + } + + // Hide header + nm.div.toggleClass('header_hidden'); + nm.set_hide_header(nm.div.hasClass('header_hidden')); + nm.resize(); + } } if(!customElements.get("et2-portlet-favorite")) diff --git a/home/js/Et2PortletList.ts b/home/js/Et2PortletList.ts new file mode 100644 index 0000000000..fd70a37ce5 --- /dev/null +++ b/home/js/Et2PortletList.ts @@ -0,0 +1,175 @@ +import {Et2Portlet} from "../../api/js/etemplate/Et2Portlet/Et2Portlet"; +import {et2_createWidget} from "../../api/js/etemplate/et2_core_widget"; +import {css, html, TemplateResult} from "@lion/core"; +import shoelace from "../../api/js/etemplate/Styles/shoelace"; + +/** + * Home portlet to show a list of entries + */ +export class Et2PortletList extends Et2Portlet +{ + static get styles() + { + return [ + ...shoelace, + ...(super.styles || []), + css` + .delete_button { + padding-right: 10px; + } + ` + ] + } + + constructor() + { + super(); + this.link_change = this.link_change.bind(this); + } + + /** + * 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) + { + // TODO + debugger; + + // Actions got confused drop vs popup + if(source[0].id == 'portlets') + { + return this.add_link(action); + } + + // Get widget + let widget = null; + while(action.parent != null) + { + if(action.data && action.data.widget) + { + widget = action.data.widget; + break; + } + action = action.parent; + } + if(target_action == null) + { + // use template base url from initial template, to continue using webdav, if that was loaded via webdav + 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) + { + // Duplicate - skip it + return; + } + } + 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}); + // Update client side + let list = widget.getWidgetById('list'); + if(list) + { + list.set_value(new_list); + } + }, + 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: ''}]} + }); + } + else + { + // Drag'n'dropped something on the list - just send action IDs + let new_list = widget.options.settings.list || []; + let changed = false; + for(let i = 0; i < new_list.length; i++) + { + // Avoid duplicates + for(let j = 0; j < source.length; j++) + { + if(!source[j].id || new_list[i].app + "::" + new_list[i].id == source[j].id) + { + // Duplicate - skip it + source.splice(j, 1); + } + } + } + for(let i = 0; i < source.length; i++) + { + 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, { + list: new_list || {} + }); + } + // Filemanager support - links need app = 'file' and type set + for(let i = 0; i < new_list.length; i++) + { + if(new_list[i]['app'] == 'filemanager') + { + new_list[i]['app'] = 'file'; + new_list[i]['path'] = new_list[i]['title'] = new_list[i]['icon'] = new_list[i]['id']; + } + } + + widget.getWidgetById('list').set_value(new_list); + } + } + + /** + * Remove a link from the list + */ + link_change(event) + { + if(!this.getInstanceManager()?.isReady) + { + return; + } + debugger; + // Not used, but delete puts link in event.data + let link_data = event.data || false; + + // Update settings on link delete + if(link_data) + { + this.update_settings({list: this.settings.list}); + } + } + + bodyTemplate() : TemplateResult + { + return html` + + ` + } + +} + +if(!customElements.get("et2-portlet-list")) +{ + customElements.define("et2-portlet-list", Et2PortletList); +} \ No newline at end of file diff --git a/home/js/app.ts b/home/js/app.ts index 028c14884c..dd2e6f31e0 100644 --- a/home/js/app.ts +++ b/home/js/app.ts @@ -8,12 +8,14 @@ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License */ -import {AppJS} from "../../api/js/jsapi/app_base.js"; import {et2_createWidget} from "../../api/js/etemplate/et2_core_widget"; 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"; +import {loadWebComponent} from "../../api/js/etemplate/Et2Widget/Et2Widget"; +import "./Et2PortletList"; +import Sortable from "sortablejs/modular/sortable.complete.esm.js"; /** * JS for home application @@ -30,7 +32,7 @@ export class HomeApp extends EgwApp /** * Grid resolution. Must match et2_portlet GRID */ - public static GRID = 50; + public static GRID = 150; /** * Default size for new portlets @@ -43,6 +45,7 @@ export class HomeApp extends EgwApp // List of portlets private portlets = {}; portlet_container : any; + private sortable : Sortable; /** * Constructor @@ -68,10 +71,13 @@ export class HomeApp extends EgwApp super.destroy(this.appname); // Make sure all other sub-etemplates in portlets are done - let others = etemplate2.getByApplication(this.appname); - for(let i = 0; i < others.length; i++) + if(this == window.app.home) { - others[i].clear(); + let others = etemplate2.getByApplication(this.appname); + for(let i = 0; i < others.length; i++) + { + others[i].clear(); + } } } @@ -96,12 +102,9 @@ export class HomeApp extends EgwApp this.portlet_container = this.et2.getWidgetById("portlets"); - // Set up sorting of portlets - //this._do_ordering(); - // Accept drops of favorites, which aren't part of action system - jQuery(this.et2.getDOMNode().parentNode).droppable({ - hoverClass: 'drop-hover', + this.sortable = new Sortable(this.et2.getDOMNode().parentNode, { + chosenClass: 'drop-hover', accept: function(draggable) { // Check for direct support for that application @@ -111,8 +114,9 @@ export class HomeApp extends EgwApp } return false; }, - drop: function(event, ui) + onAdd: function(event, ui) { + debugger; // Favorite dropped on home - fake an action and divert to normal handler let action = { data: { @@ -128,8 +132,9 @@ export class HomeApp extends EgwApp 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) { if(e.target && e.target.id && this.portlets[e.target.id]) @@ -138,6 +143,8 @@ export class HomeApp extends EgwApp delete this.portlets[e.target.id]; } }, this)); + + */ } else if(et2.uniqueId) { @@ -148,7 +155,7 @@ export class HomeApp extends EgwApp window.setTimeout(() => {this.et2_ready(et2, name);}, 200); return; } - let portlet = portlet_container.getWidgetById(et2.uniqueId); + let portlet = portlet_container.getWidgetById(et2.uniqueId) || et2.DOMContainer; // Check for existing etemplate, this one loaded over it // NOTE: Moving them around like this can cause problems with event handlers let existing = etemplate2.getById(et2.uniqueId); @@ -163,14 +170,19 @@ export class HomeApp extends EgwApp } } // Set size & position - let settings = portlet_container.getArrayMgr("content").data.find(e => e.id == et2.uniqueId) || {}; + let et2_data = et2.widgetContainer.getArrayMgr("content").data; + let settings = et2_data && et2_data.id == portlet.id && et2_data || portlet_container.getArrayMgr("content").data.find(e => et2.uniqueId.endsWith(e.id)) || {settings: {}}; + portlet.settings = settings.settings || {}; 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 + let misplaced = jQuery(etemplate2.getById('home-index').DOMContainer).siblings('#' + et2.DOMContainer.id); - if(portlet) + + if(portlet && et2.DOMContainer !== portlet) { - portlet.addChild(et2.widgetContainer); + portlet.append(et2.DOMContainer); et2.resize(); } if(portlet && misplaced.length) @@ -180,6 +192,10 @@ export class HomeApp extends EgwApp // Instanciate custom code for this portlet this._get_portlet_code(portlet); + + // Ordering of portlets + // Only needs to be done once, but its hard to tell when everything is loaded + this._do_ordering(); } } @@ -248,10 +264,11 @@ export class HomeApp extends EgwApp { 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); + // Use "auto" col to avoid any overlap or overflow + attrs.col = "auto"; } - let portlet = et2_createWidget('et2-portlet', attrs, this.portlet_container); + let portlet = loadWebComponent('et2-portlet', attrs, this.portlet_container); portlet.loadingFinished(); // Get actual attributes & settings, since they're not available client side yet @@ -277,48 +294,38 @@ export class HomeApp extends EgwApp // Basic portlet attributes let attrs = { + ...HomeApp.DEFAULT, id: this._create_id(), class: action.data.class || action.id.substr(5), - width: this.DEFAULT.WIDTH, - height: this.DEFAULT.HEIGHT + dropped_data: [] }; // 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) / HomeApp.GRID)); + // Use "auto" col to avoid any overlap or overflow + attrs.col = "auto"; } - 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('
'); - // Get actual attributes & settings, since they're not available client side yet - let drop_data = []; for(let i = 0; i < source.length; i++) { if(source[i].id) { - drop_data.push(source[i].id); + attrs.dropped_data.push(source[i].id); } else { - drop_data.push(source[i].data); + attrs.dropped_data.push(source[i].data); } } - // 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)); - // Set up sorting/grid of new portlet - $portlet_container.data("gridster").add_widget( - portlet.getDOMNode(), - this.DEFAULT.WIDTH, this.DEFAULT.HEIGHT, - attrs.col, attrs.row - ); + let portlet = loadWebComponent('et2-portlet', attrs, this.portlet_container); + portlet.loadingFinished(); + + // Get actual attributes & settings, since they're not available client side yet + portlet.update_settings(attrs); // Instanciate custom code for this portlet this._get_portlet_code(portlet); @@ -399,7 +406,7 @@ export class HomeApp extends EgwApp let p = this.portlet_container.getWidgetById(id); if(p) { - p._process_edit(et2_dialog.OK_BUTTON, '~reload~'); + p.update_settings('~reload~'); } } @@ -462,95 +469,25 @@ export class HomeApp extends EgwApp */ _do_ordering() { - let $portlet_container = jQuery(this.portlet_container.getDOMNode()); - $portlet_container - /* Gridster */ - .gridster({ - widget_selector: 'div.et2_portlet', - // Dimensions + margins = grid spacing - widget_base_dimensions: [home.GRID - 5, home.GRID - 5], - widget_margins: [5, 5], - extra_rows: 1, - extra_cols: 1, - min_cols: 3, - min_rows: 3, - /** - * Set which parameters we want when calling serialize(). - * @param $w jQuery jQuery-wrapped element - * @param grid Object Grid settings - * @return Object - will be returned by gridster.serialize() - */ - serialize_params: function($w, grid) - { - return { - id: $w.attr('id').replace(app.home.portlet_container.getInstanceManager().uniqueId + '_', ''), - row: grid.row, - col: grid.col, - width: grid.size_x, - height: grid.size_y - }; - }, - /** - * Gridster's internal drag settings - */ - draggable: { - handle: '.ui-widget-header', - stop: function(event, ui) - { - // Update widget(s) - let changed = this.serialize_changed(); - - // Reset changed, or they keep accumulating - this.$changed = jQuery([]); - - for(let key in changed) - { - if(!changed[key].id) - { - continue; - } - // Changed ID is the ID - 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, {}, { - row: changed[key].row, - col: changed[key].col - }, widget.settings ? widget.settings.group : false], - null, - widget, true, widget - ); - } - } - } - - }); - - // Rescue selectboxes from Firefox - $portlet_container.on('mousedown touchstart', 'select', function(e) + if(!this.portlet_container) { - e.stopPropagation(); - }); - // Bind window resize to re-layout gridster - jQuery(window).one("resize." + this.et2._inst.uniqueId, function() + return; + } + + let col_map = {}; + this.portlet_container.getDOMNode().querySelectorAll("[style*='grid-area']").forEach((n) => { - // 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) + let [col, span] = (getComputedStyle(n).gridColumn || "").split(" / "); + if(typeof col_map[col] !== "undefined") { - $portlet_container.data("gridster").resize_widget( - ui.element, - Math.round(ui.size.width / app.home.GRID), - Math.round(ui.size.height / app.home.GRID) - ); - }); + // Set column to auto to avoid overlap + n.style.gridColumn = "auto / " + span; + } + else + { + col_map[col] = true; + } + }); } /** @@ -572,126 +509,12 @@ export class HomeApp extends EgwApp /** * 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(action, source, target_action) - { - // Actions got confused drop vs popup - if(source[0].id == 'portlets') - { - return this.add_link(action); - } - - // Get widget - let widget = null; - while(action.parent != null) - { - if(action.data && action.data.widget) - { - widget = action.data.widget; - break; - } - action = action.parent; - } - if(target_action == null) - { - // use template base url from initial template, to continue using webdav, if that was loaded via webdav - 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) - { - // Duplicate - skip it - return; - } - } - 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}); - // Update client side - let list = widget.getWidgetById('list'); - if(list) - { - list.set_value(new_list); - } - }, - 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: ''}]} - }); - } - else - { - // Drag'n'dropped something on the list - just send action IDs - let new_list = widget.options.settings.list || []; - let changed = false; - for(let i = 0; i < new_list.length; i++) - { - // Avoid duplicates - for(let j = 0; j < source.length; j++) - { - if(!source[j].id || new_list[i].app + "::" + new_list[i].id == source[j].id) - { - // Duplicate - skip it - source.splice(j, 1); - } - } - } - for(let i = 0; i < source.length; i++) - { - 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, { - list: new_list || {} - }); - } - // Filemanager support - links need app = 'file' and type set - for(let i = 0; i < new_list.length; i++) - { - if(new_list[i]['app'] == 'filemanager') - { - new_list[i]['app'] = 'file'; - new_list[i]['path'] = new_list[i]['title'] = new_list[i]['icon'] = new_list[i]['id']; - } - } - - widget.getWidgetById('list').set_value(new_list); - } - } - /** * Remove a link from the list */ link_change(list, link_id, row) { - // Quick response client side - row.slideUp(row.remove); - - // Actual removal - 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 || {}}); + list.link_change(link_id, row); } /** diff --git a/home/templates/default/app.css b/home/templates/default/app.css index bd1ad0ff6b..452902d7ec 100644 --- a/home/templates/default/app.css +++ b/home/templates/default/app.css @@ -11,8 +11,9 @@ #home-index_portlets { background-color: inherit; display: grid; - grid-auto-columns: 50ex; - grid-auto-rows: 50ex; + grid-auto-columns: 25ex; + grid-auto-rows: 25ex; + grid-auto-flow: dense; gap: 2ex; diff --git a/home/templates/default/favorite.xet b/home/templates/default/favorite.xet index 4159889141..9b7d215ed8 100644 --- a/home/templates/default/favorite.xet +++ b/home/templates/default/favorite.xet @@ -2,7 +2,6 @@ \ No newline at end of file diff --git a/home/templates/default/index.xet b/home/templates/default/index.xet index b4341be2de..b2fed3843c 100644 --- a/home/templates/default/index.xet +++ b/home/templates/default/index.xet @@ -8,26 +8,13 @@ + + + + + + \ No newline at end of file