/**
* 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}