Home WIP Favorites working a little better

This commit is contained in:
nathan 2023-03-03 15:41:56 -07:00
parent 59c4070733
commit d37143c842
13 changed files with 478 additions and 302 deletions

View File

@ -13,8 +13,9 @@
import {Et2Widget} from "../Et2Widget/Et2Widget"; import {Et2Widget} from "../Et2Widget/Et2Widget";
import {SlCard} from "@shoelace-style/shoelace"; import {SlCard} from "@shoelace-style/shoelace";
import interact from "@interactjs/interactjs"; import interact from "@interactjs/interactjs";
import type {InteractEvent} from "@interactjs/core/InteractEvent";
import {egw} from "../../jsapi/egw_global"; 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 {HasSlotController} from "@shoelace-style/shoelace/dist/internal/slot";
import shoelace from "../Styles/shoelace"; import shoelace from "../Styles/shoelace";
import {Et2Dialog} from "../Et2Dialog/Et2Dialog"; import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
@ -25,7 +26,7 @@ import {HomeApp} from "../../../../home/js/app";
* Participate in Home * Participate in Home
*/ */
export class Et2Portlet extends (Et2Widget(SlCard)) export class Et2Portlet extends Et2Widget(SlCard)
{ {
static get properties() static get properties()
{ {
@ -62,8 +63,12 @@ export class Et2Portlet extends (Et2Widget(SlCard))
...shoelace, ...shoelace,
...(super.styles || []), ...(super.styles || []),
css` css`
:host {
--header-spacing: var(--sl-spacing-medium);
}
.portlet__header { .portlet__header {
flex: 1 0 auto; flex: 0 0 auto;
display: flex; display: flex;
font-style: inherit; font-style: inherit;
font-variant: inherit; font-variant: inherit;
@ -73,7 +78,9 @@ export class Et2Portlet extends (Et2Widget(SlCard))
font-size: var(--sl-font-size-medium); font-size: var(--sl-font-size-medium);
line-height: var(--sl-line-height-dense); line-height: var(--sl-line-height-dense);
padding: var(--header-spacing); padding: var(--header-spacing);
padding-right: calc(2em + var(--header-spacing));
margin: 0px; margin: 0px;
position: relative;
} }
.portlet__title { .portlet__title {
@ -84,8 +91,8 @@ export class Et2Portlet extends (Et2Widget(SlCard))
.portlet__header et2-button-icon { .portlet__header et2-button-icon {
display: none; display: none;
order: 99; position: absolute;
margin-left: auto; right: 0px;
} }
.portlet__header:hover et2-button-icon { .portlet__header:hover et2-button-icon {
@ -97,11 +104,17 @@ export class Et2Portlet extends (Et2Widget(SlCard))
height: 100% height: 100%
} }
.card_header {
margin-right: calc(var(--sl-spacing-medium) + 1em);
}
.card__body { .card__body {
/* display block to prevent overflow from our size */ /* display block to prevent overflow from our size */
display: block; display: block;
overflow: hidden; overflow: hidden;
flex: 1 1 auto; flex: 1 1 auto;
padding: 0px;
} }
@ -151,6 +164,19 @@ export class Et2Portlet extends (Et2Widget(SlCard))
.then(() => this._setupMoveResize()); .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 * Overriden from parent to add in default actions
*/ */
@ -316,16 +342,17 @@ export class Et2Portlet extends (Et2Widget(SlCard))
headerTemplate() headerTemplate()
{ {
return html` return html`
<header class="portlet__header"> <h2 class="portlet__title">${this.title}</h2>`;
<h2 class="portlet__title">${this.title}</h2>
<et2-button-icon name="gear" label="Settings" noSubmit=true
@click="${() => this.edit_settings()}"></et2-button-icon>
</header>`;
} }
footerTemplate() bodyTemplate() : TemplateResult
{ {
return ''; return html``;
}
footerTemplate() : TemplateResult
{
return html``;
} }
@ -343,7 +370,27 @@ export class Et2Portlet extends (Et2Widget(SlCard))
value: { value: {
content: this.settings 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 // Set separately to avoid translation
dialog.title = this.egw().lang("Edit") + " " + (this.title || ''); 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 != Et2Dialog.OK_BUTTON)
{ {
if(button_id == "delete")
{
this.update_settings('~remove~').then(() =>
{
this.remove();
});
}
return; return;
} }
// Pass updated settings, unless we're removing // 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 // Extend, not replace, because settings has types while value has just value
if(typeof value == 'object') 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, // Save settings - server might reply with new content if the portlet needs an update,
// but ideally it doesn't // but ideally it doesn't
this.classList.add("loading"); 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) function(data)
{ {
// This section not for us // This section not for us
@ -437,8 +497,13 @@ export class Et2Portlet extends (Et2Widget(SlCard))
})} })}
> >
<slot name="image" part="image" class="card__image">${this.imageTemplate()}</slot> <slot name="image" part="image" class="card__image">${this.imageTemplate()}</slot>
<header class="portlet__header">
<slot name="header" part="header" class="card__header">${this.headerTemplate()}</slot> <slot name="header" part="header" class="card__header">${this.headerTemplate()}</slot>
<slot part="body" class="card__body"></slot> <et2-button-icon name="gear" label="Settings" noSubmit=true
@click="${() => this.edit_settings()}"></et2-button-icon>
</header>
<slot part="body" class="card__body">${this.bodyTemplate()}</slot>
<slot name="footer" part="footer" class="card__footer">${this.footerTemplate()}</slot> <slot name="footer" part="footer" class="card__footer">${this.footerTemplate()}</slot>
</div> </div>
`; `;

View File

@ -94,7 +94,7 @@ export class et2_box extends et2_baseWidget implements et2_IDetachedDOM
// Create the new element, if no expansion needed // Create the new element, if no expansion needed
var id = et2_readAttrWithDefault(node, "id", ""); 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); this.createElementFromNode(node);
childIndex++; childIndex++;

View File

@ -148,13 +148,54 @@ class home_favorite_portlet extends home_portlet
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. // We need to keep the template going, thanks.
Etemplate\Widget::setElementAttribute('','',''); Etemplate\Widget::setElementAttribute('', '', '');
} }
public function get_actions(){ public function get_actions()
{
return array(); 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 * 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. * they want it or not, and for inclusion in lists, hover text, etc.

View File

@ -92,12 +92,17 @@ class home_list_portlet extends home_portlet
public function get_description() public function get_description()
{ {
return array( return array(
'displayName'=> 'List of entries', 'displayName' => 'List of entries',
'title'=> $this->title, 'title' => $this->title,
'description'=> lang('Show a list of entries') 'description' => lang('Show a list of entries')
); );
} }
public function get_type()
{
return 'et2-portlet-list';
}
/** /**
* Get a fragment of HTML for display * Get a fragment of HTML for display
* *
@ -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);
} }
/** /**

View File

@ -49,6 +49,34 @@ abstract class home_portlet
*/ */
public abstract function get_description(); 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 * Generate the display for the portlet
* *

View File

@ -115,8 +115,7 @@ class home_ui
'onExecute' => 'javaScript:app.home.add', 'onExecute' => 'javaScript:app.home.add',
'children' => $add_portlets 'children' => $add_portlets
), ),
// Favorites are sortable which needs special handling, // Favorites are sortable which needs special handling
// handled directly through jQuery
); );
// Add all known portlets as drop actions too. If there are multiple matches, there will be a menu // Add all known portlets as drop actions too. If there are multiple matches, there will be a menu
@ -180,7 +179,8 @@ class home_ui
$portlet = new $classname($context); $portlet = new $classname($context);
$desc = $portlet->get_description(); $desc = $portlet->get_description();
$portlet_content = array( $portlet_content = array(
'id' => $id 'id' => $id,
'type' => $portlet->get_type()
) + $desc + $context; ) + $desc + $context;
@ -566,6 +566,7 @@ class home_ui
else else
{ {
$prefs->delete('home', $portlet_id); $prefs->delete('home', $portlet_id);
$response->data([]);
} }
} }
else else
@ -594,7 +595,7 @@ class home_ui
$classname = substr($classname, 4); $classname = substr($classname, 4);
} }
$content = null; $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); $context['class'] = get_class($portlet);
foreach($portlet->get_properties() as $property) foreach($portlet->get_properties() as $property)

View File

@ -39,7 +39,7 @@ class home_weather_portlet extends home_portlet
if (false) parent::__construct(); if (false) parent::__construct();
// City not set for new widgets created via context menu // 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 // Set initial size to 3x2, default is too small
$context['width'] = 3; $context['width'] = 3;

View File

@ -1,7 +1,58 @@
import {Et2Portlet} from "../../api/js/etemplate/Et2Portlet/Et2Portlet"; 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 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()}
<et2-button id="header_toggle" slot="header" image="egw_action/arrows" class="closed"
noSubmit=true
@click=${this.toggleHeader}
></et2-button>
`;
}
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")) if(!customElements.get("et2-portlet-favorite"))

175
home/js/Et2PortletList.ts Normal file
View File

@ -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`
<et2-link-list .value=${this.settings?.list || []}
@change=${this.link_change}
>
</et2-link-list>`
}
}
if(!customElements.get("et2-portlet-list"))
{
customElements.define("et2-portlet-list", Et2PortletList);
}

View File

@ -8,12 +8,14 @@
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @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 {et2_createWidget} from "../../api/js/etemplate/et2_core_widget";
import {EgwApp} from "../../api/js/jsapi/egw_app"; import {EgwApp} from "../../api/js/jsapi/egw_app";
import {etemplate2} from "../../api/js/etemplate/etemplate2"; import {etemplate2} from "../../api/js/etemplate/etemplate2";
import {Et2Portlet} from "../../api/js/etemplate/Et2Portlet/Et2Portlet"; import {Et2Portlet} from "../../api/js/etemplate/Et2Portlet/Et2Portlet";
import {Et2PortletFavorite} from "./Et2PortletFavorite"; 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 * JS for home application
@ -30,7 +32,7 @@ export class HomeApp extends EgwApp
/** /**
* Grid resolution. Must match et2_portlet GRID * Grid resolution. Must match et2_portlet GRID
*/ */
public static GRID = 50; public static GRID = 150;
/** /**
* Default size for new portlets * Default size for new portlets
@ -43,6 +45,7 @@ export class HomeApp extends EgwApp
// List of portlets // List of portlets
private portlets = {}; private portlets = {};
portlet_container : any; portlet_container : any;
private sortable : Sortable;
/** /**
* Constructor * Constructor
@ -68,12 +71,15 @@ export class HomeApp extends EgwApp
super.destroy(this.appname); super.destroy(this.appname);
// Make sure all other sub-etemplates in portlets are done // Make sure all other sub-etemplates in portlets are done
if(this == window.app.home)
{
let others = etemplate2.getByApplication(this.appname); let others = etemplate2.getByApplication(this.appname);
for(let i = 0; i < others.length; i++) for(let i = 0; i < others.length; i++)
{ {
others[i].clear(); others[i].clear();
} }
} }
}
/** /**
* This function is called when the etemplate2 object is loaded * This function is called when the etemplate2 object is loaded
@ -96,12 +102,9 @@ export class HomeApp extends EgwApp
this.portlet_container = this.et2.getWidgetById("portlets"); 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 // Accept drops of favorites, which aren't part of action system
jQuery(this.et2.getDOMNode().parentNode).droppable({ this.sortable = new Sortable(this.et2.getDOMNode().parentNode, {
hoverClass: 'drop-hover', chosenClass: 'drop-hover',
accept: function(draggable) accept: function(draggable)
{ {
// Check for direct support for that application // Check for direct support for that application
@ -111,8 +114,9 @@ export class HomeApp extends EgwApp
} }
return false; return false;
}, },
drop: function(event, ui) onAdd: function(event, ui)
{ {
debugger;
// Favorite dropped on home - fake an action and divert to normal handler // Favorite dropped on home - fake an action and divert to normal handler
let action = { let action = {
data: { data: {
@ -128,8 +132,9 @@ export class HomeApp extends EgwApp
action.ui = ui; action.ui = ui;
app.home.add_from_drop(action, [{data: ui.helper.context.dataset}]) app.home.add_from_drop(action, [{data: ui.helper.context.dataset}])
} }
}) });
// Bind to unload to remove it from our list // Bind to unload to remove it from our list
/*
.on('clear', '.et2_container[id]', jQuery.proxy(function(e) .on('clear', '.et2_container[id]', jQuery.proxy(function(e)
{ {
if(e.target && e.target.id && this.portlets[e.target.id]) 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]; delete this.portlets[e.target.id];
} }
}, this)); }, this));
*/
} }
else if(et2.uniqueId) else if(et2.uniqueId)
{ {
@ -148,7 +155,7 @@ export class HomeApp extends EgwApp
window.setTimeout(() => {this.et2_ready(et2, name);}, 200); window.setTimeout(() => {this.et2_ready(et2, name);}, 200);
return; 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 // Check for existing etemplate, this one loaded over it
// NOTE: Moving them around like this can cause problems with event handlers // NOTE: Moving them around like this can cause problems with event handlers
let existing = etemplate2.getById(et2.uniqueId); let existing = etemplate2.getById(et2.uniqueId);
@ -163,14 +170,19 @@ export class HomeApp extends EgwApp
} }
} }
// Set size & position // 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); 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 // 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); 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(); et2.resize();
} }
if(portlet && misplaced.length) if(portlet && misplaced.length)
@ -180,6 +192,10 @@ export class HomeApp extends EgwApp
// Instanciate custom code for this portlet // Instanciate custom code for this portlet
this._get_portlet_code(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()); 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.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 = <Et2Portlet>et2_createWidget('et2-portlet', attrs, this.portlet_container); let portlet = <Et2Portlet>loadWebComponent('et2-portlet', attrs, this.portlet_container);
portlet.loadingFinished(); portlet.loadingFinished();
// Get actual attributes & settings, since they're not available client side yet // Get actual attributes & settings, since they're not available client side yet
@ -277,48 +294,38 @@ export class HomeApp extends EgwApp
// Basic portlet attributes // Basic portlet attributes
let attrs = { let attrs = {
...HomeApp.DEFAULT,
id: this._create_id(), id: this._create_id(),
class: action.data.class || action.id.substr(5), class: action.data.class || action.id.substr(5),
width: this.DEFAULT.WIDTH, dropped_data: []
height: this.DEFAULT.HEIGHT
}; };
// Try to find where the drop was // Try to find where the drop was
if(action != null && action.ui && action.ui.position) 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.row = Math.max(1, Math.round((action.ui.position.top - $portlet_container.offset().top) / HomeApp.GRID));
attrs.col = Math.max(1, Math.round((action.ui.position.left - $portlet_container.offset().left) / this.GRID)); // Use "auto" col to avoid any overlap or overflow
attrs.col = "auto";
} }
let portlet = <Et2Portlet>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('<div id="' + attrs.id + '" class="et2_container"/>');
// Get actual attributes & settings, since they're not available client side yet // Get actual attributes & settings, since they're not available client side yet
let drop_data = [];
for(let i = 0; i < source.length; i++) for(let i = 0; i < source.length; i++)
{ {
if(source[i].id) if(source[i].id)
{ {
drop_data.push(source[i].id); attrs.dropped_data.push(source[i].id);
} }
else 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 let portlet = <Et2Portlet>loadWebComponent('et2-portlet', attrs, this.portlet_container);
$portlet_container.data("gridster").add_widget( portlet.loadingFinished();
portlet.getDOMNode(),
this.DEFAULT.WIDTH, this.DEFAULT.HEIGHT, // Get actual attributes & settings, since they're not available client side yet
attrs.col, attrs.row portlet.update_settings(attrs);
);
// Instanciate custom code for this portlet // Instanciate custom code for this portlet
this._get_portlet_code(portlet); this._get_portlet_code(portlet);
@ -399,7 +406,7 @@ export class HomeApp extends EgwApp
let p = this.portlet_container.getWidgetById(id); let p = this.portlet_container.getWidgetById(id);
if(p) if(p)
{ {
p._process_edit(et2_dialog.OK_BUTTON, '~reload~'); p.update_settings('~reload~');
} }
} }
@ -462,94 +469,24 @@ export class HomeApp extends EgwApp
*/ */
_do_ordering() _do_ordering()
{ {
let $portlet_container = jQuery(this.portlet_container.getDOMNode()); if(!this.portlet_container)
$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 { 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, {}, { let col_map = {};
row: changed[key].row, this.portlet_container.getDOMNode().querySelectorAll("[style*='grid-area']").forEach((n) =>
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)
{ {
e.stopPropagation(); let [col, span] = (getComputedStyle(n).gridColumn || "").split(" / ");
}); if(typeof col_map[col] !== "undefined")
// Bind window resize to re-layout gridster
jQuery(window).one("resize." + this.et2._inst.uniqueId, function()
{ {
// Note this doesn't change the positions, just makes them invalid // Set column to auto to avoid overlap
$portlet_container.data('gridster').recalculate_faux_grid(); n.style.gridColumn = "auto / " + span;
}); }
// Bind resize to update gridster - this may happen _before_ the widget gets a else
// chance to update itself, so we can't use the widget
$portlet_container
.on("resizestop", function(event, ui)
{ {
$portlet_container.data("gridster").resize_widget( col_map[col] = true;
ui.element, }
Math.round(ui.size.width / app.home.GRID),
Math.round(ui.size.height / app.home.GRID)
);
}); });
} }
@ -572,126 +509,12 @@ export class HomeApp extends EgwApp
/** /**
* Functions for the list portlet * 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 * Remove a link from the list
*/ */
link_change(list, link_id, row) link_change(list, link_id, row)
{ {
// Quick response client side list.link_change(link_id, row);
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 || {}});
} }
/** /**

View File

@ -11,8 +11,9 @@
#home-index_portlets { #home-index_portlets {
background-color: inherit; background-color: inherit;
display: grid; display: grid;
grid-auto-columns: 50ex; grid-auto-columns: 25ex;
grid-auto-rows: 50ex; grid-auto-rows: 25ex;
grid-auto-flow: dense;
gap: 2ex; gap: 2ex;

View File

@ -2,7 +2,6 @@
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd"> <!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd">
<overlay> <overlay>
<template id="home.favorite" template="" lang="" group="0" version="1.9.001"> <template id="home.favorite" template="" lang="" group="0" version="1.9.001">
<et2-button id="header_toggle" image="egw_action/arrows" class="closed" onclick="app.home.nextmatch_toggle_header" parentId="@header_node"></et2-button>
<nextmatch id="nm" class="header_hidden" hide_header="true"/> <nextmatch id="nm" class="header_hidden" hide_header="true"/>
</template> </template>
</overlay> </overlay>

View File

@ -8,26 +8,13 @@
</et2-vbox> </et2-vbox>
</template> </template>
<template id="home.index" template="" lang="" group="0" version="1.9.001"> <template id="home.index" template="" lang="" group="0" version="1.9.001">
<grid width="100%">
<columns>
<column/>
</columns>
<rows>
<row disabled="!@mainscreen_message">
<html id="mainscreen_message"/>
</row>
<row>
<old-box id="portlets"> <old-box id="portlets">
<!-- Box wrapper needed to get box to auto-repeat --> <!-- Box wrapper needed to get box to auto-repeat -->
<et2-box id="${row}"> <et2-portlet type="${row_cont[type]}" id="${row_cont[id]}" title="$row_cont[title]" color="$row_cont[color]"
<et2-portlet id="${_cont[id]}" title="$_cont[title]" color="@color" parent_node="home-index_portlets" settings="$row_cont[settings]"
parent_node="home-index_portlets" settings="@settings" width="@width" value="$row_cont[content]" class="$row_cont[class]"
height="@height" row="@row" col="@col" value="@content" class="@class" actions="$row_cont[actions]"/>
actions="@actions"/>
</et2-box>
</old-box> </old-box>
</row>
</rows>
</grid>
</template> </template>
</overlay> </overlay>