Merge web-components branch

This commit is contained in:
nathan 2021-12-06 11:17:43 -07:00
commit e3ca8b8f7f
60 changed files with 13298 additions and 958 deletions

View File

@ -83,9 +83,7 @@
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_fax,&amp;hearts;"/> <radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_fax,&amp;hearts;"/>
<description for="adr_one_countryname" value="country"/> <description for="adr_one_countryname" value="country"/>
<vbox class="city_state_postcode" width="100%"> <vbox class="city_state_postcode" width="100%">
<menulist class="et2_fullWidth"> <select type="select-country" tags="true" width="100%" class="countrySelect et2_fullWidth" id="adr_one_countrycode" tabindex="15" onchange="app.addressbook.show_custom_country(this);" options="Select one,0,1" autocomplete="country"/>
<menupopup type="select-country" submit="1" tags="true" width="100%" class="countrySelect et2_fullWidth" id="adr_one_countrycode" tabindex="15" onchange="app.addressbook.show_custom_country(this);" options="Select one,0,1" autocomplete="country"/>
</menulist>
<textbox id="adr_one_countryname" class="custom_country et2_fullWidth" autocomplete="country-name"/> <textbox id="adr_one_countryname" class="custom_country et2_fullWidth" autocomplete="country-name"/>
</vbox> </vbox>
<description/> <description/>
@ -116,9 +114,9 @@
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_fax,&amp;hearts;"/> <radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_fax,&amp;hearts;"/>
<description for="adr_one_countryname" value="country"/> <description for="adr_one_countryname" value="country"/>
<vbox width="100%" tabindex="16"> <vbox width="100%" tabindex="16">
<menulist tabindex="16"> <select-country tabindex="16" tags="true" width="100%" class="countrySelect et2_fullWidth"
<menupopup type="select-country" tabindex="16" tags="true" width="100%" class="countrySelect et2_fullWidth" id="adr_one_countrycode" onchange="app.addressbook.show_custom_country(this);" options="Select one,0,1" autocomplete="country"/> id="adr_one_countrycode" onchange="app.addressbook.show_custom_country(this);"
</menulist> options="Select one,0,1" autocomplete="country"/>
<textbox id="adr_one_countryname" class="custom_country et2_fullWidth" tabindex="16" autocomplete="country-name"/> <textbox id="adr_one_countryname" class="custom_country et2_fullWidth" tabindex="16" autocomplete="country-name"/>
</vbox> </vbox>
<description/> <description/>
@ -137,9 +135,7 @@
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_car,&amp;hearts;"/> <radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_car,&amp;hearts;"/>
<description disabled="@no_tid" for="tid" value="Type"/> <description disabled="@no_tid" for="tid" value="Type"/>
<hbox width="102%"> <hbox width="102%">
<menulist disabled="@no_tid"> <select id="tid" no_lang="1" class="et2_fullWidth" onchange="1" disabled="@no_tid"/>
<menupopup id="tid" no_lang="1" class="et2_fullWidth" onchange="1"/>
</menulist>
</hbox> </hbox>
<description/> <description/>
</row> </row>
@ -247,9 +243,9 @@
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_pager,&amp;hearts;"/> <radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_pager,&amp;hearts;"/>
<description for="adr_two_countryname" value="country"/> <description for="adr_two_countryname" value="country"/>
<vbox width="100%" class="city_state_postcode"> <vbox width="100%" class="city_state_postcode">
<menulist class="et2_fullWidth"> <select-country tabindex="37" tags="true" width="100%" class="countrySelect et2_fullWidth"
<menupopup type="select-country" tabindex="37" tags="true" width="100%" class="countrySelect et2_fullWidth" id="adr_two_countrycode" onchange="app.addressbook.show_custom_country(this);" options="Select one,0,1" autocomplete="section-two country" /> id="adr_two_countrycode" onchange="app.addressbook.show_custom_country(this);"
</menulist> options="Select one,0,1" autocomplete="section-two country"/>
<textbox id="adr_two_countryname" class="custom_country et2_fullWidth" autocomplete="section-two country-name" /> <textbox id="adr_two_countryname" class="custom_country et2_fullWidth" autocomplete="section-two country-name" />
</vbox> </vbox>
<description/> <description/>
@ -280,9 +276,9 @@
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_pager,&amp;hearts;"/> <radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_pager,&amp;hearts;"/>
<description for="adr_two_countryname" value="country"/> <description for="adr_two_countryname" value="country"/>
<vbox width="100%"> <vbox width="100%">
<menulist> <select-country class="countrySelect et2_fullWidth" tags="true" width="100%"
<menupopup type="select-country" class="countrySelect et2_fullWidth" tags="true" width="100%" id="adr_two_countrycode" onchange="app.addressbook.show_custom_country(this);" options="Select one,0,1" autocomplete="section-two country" /> id="adr_two_countrycode" onchange="app.addressbook.show_custom_country(this);"
</menulist> options="Select one,0,1" autocomplete="section-two country"/>
<textbox id="adr_two_countryname" class="custom_country et2_fullWidth" autocomplete="section-two country-name" /> <textbox id="adr_two_countryname" class="custom_country et2_fullWidth" autocomplete="section-two country-name" />
</vbox> </vbox>
<description/> <description/>
@ -416,9 +412,9 @@
</row> </row>
<row class="dialogOperators"> <row class="dialogOperators">
<description value="Addressbook"/> <description value="Addressbook"/>
<menulist class="et2_fullWidth"> <select class="owner et2_fullWidth" statustext="Addressbook the contact should be saved to"
<menupopup class="owner" statustext="Addressbook the contact should be saved to" id="owner" no_lang="1" onchange="widget.getInstanceManager().submit(null,false,true); return false;" /> id="owner" no_lang="1"
</menulist> onchange="widget.getInstanceManager().submit(null,false,true); return false;"/>
<description/> <description/>
<description value="own sorting"/> <description value="own sorting"/>
<menulist> <menulist>

View File

@ -232,7 +232,6 @@ select#addressbook-index_col_filter\[tid\] {
#addressbook-edit { #addressbook-edit {
height: auto; height: auto;
min-height: 390px; min-height: 390px;
overflow: auto;
} }
/*##################################################################*/ /*##################################################################*/
/*Infolog*/ /*Infolog*/

View File

@ -84,7 +84,6 @@
#addressbook-edit { #addressbook-edit {
height: auto; height: auto;
min-height: 390px; min-height: 390px;
overflow: auto;
} }
/*##################################################################*/ /*##################################################################*/

View File

@ -175,15 +175,15 @@ class AdminApp extends EgwApp
if(ajax) if(ajax)
{ {
if(this.ajax_target.node.children.length) if(this.ajax_target.getDOMNode().children.length)
{ {
// Node has children already? Check for loading over an // Node has children already? Check for loading over an
// existing etemplate, and remove it first // existing etemplate, and remove it first
jQuery(this.ajax_target.node.children).each(function() { jQuery(this.ajax_target.getDOMNode().children).each(function() {
var old = etemplate2.getById(this.id); var old = etemplate2.getById(this.id);
if(old) old.clear(); if(old) old.clear();
}); });
jQuery(this.ajax_target.node).empty(); jQuery(this.ajax_target.getDOMNode()).empty();
} }
this.egw.json( this.egw.json(
framework.activeApp.getMenuaction('ajax_exec', _url), framework.activeApp.getMenuaction('ajax_exec', _url),
@ -394,7 +394,7 @@ class AdminApp extends EgwApp
if(!_data || _data.type != undefined) return; if(!_data || _data.type != undefined) return;
// Insert the content, etemplate will load into it // Insert the content, etemplate will load into it
jQuery(this.ajax_target.node).append(typeof _data === 'string' ? _data : _data[0]); jQuery(this.ajax_target.getDOMNode()).append(typeof _data === 'string' ? _data : _data[0]);
} }
/** /**

126
api/etemplate.php Normal file
View File

@ -0,0 +1,126 @@
<?php
/**
* API: loading for web-components modified eTemplate from server
*
* Usage: /egroupware/api/etemplate.php/<app>/templates/default/<name>.xet
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb@egroupware-org>
* @package api
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
use EGroupware\Api;
// add et2- prefix to following widgets/tags
const ADD_ET2_PREFIX_REGEXP = '#<((/?)([vh]?box|textbox|textarea|button))(/?|\s[^>]*)>#m';
// switch evtl. set output-compression off, as we cant calculate a Content-Length header with transparent compression
ini_set('zlib.output_compression', 0);
$GLOBALS['egw_info'] = array(
'flags' => array(
'currentapp' => 'api',
'noheader' => true,
// miss-use session creation callback to send the template, in case we have no session
'autocreate_session_callback' => 'send_template',
'nocachecontrol' => true,
)
);
$start = microtime(true);
include '../header.inc.php';
send_template();
function send_template()
{
$header_include = microtime(true);
// release session, as we don't need it and it blocks parallel requests
$GLOBALS['egw']->session->commit_session();
header('Content-Type: application/xml; charset=UTF-8');
//$path = EGW_SERVER_ROOT.$_SERVER['PATH_INFO'];
// check for customized template in VFS
list(, $app, , $template, $name) = explode('/', $_SERVER['PATH_INFO']);
$path = Api\Etemplate::rel2path(Api\Etemplate::relPath($app . '.' . basename($name, '.xet'), $template));
if(empty($path) || !file_exists($path) || !is_readable($path))
{
http_response_code(404);
exit;
}
/* disable caching for now, as you need to delete the cache, once you change ADD_ET2_PREFIX_REGEXP
$cache = $GLOBALS['egw_info']['server']['temp_dir'].'/egw_cache/eT2-Cache-'.$GLOBALS['egw_info']['server']['install_id'].$_SERVER['PATH_INFO'];
if (file_exists($cache) && filemtime($cache) > filemtime($path) &&
($str = file_get_contents($cache)) !== false)
{
$cache_read = microtime(true);
}
else*/
if(($str = file_get_contents($path)) !== false)
{
// fix <menulist...><menupopup type="select-*"/></menulist> --> <select type="select-*" .../>
$str = preg_replace('#<menulist([^>]*)>[\r\n\s]*<menupopup([^>]+>)[\r\n\s]*</menulist>#', '<select$1$2', $str);
// fix <textbox multiline="true" .../> --> <textarea .../> (et2-prefix and self-closing is handled below)
$str = preg_replace('#<textbox(.*?)\smultiline="true"(.*?)/>#u', '<textarea$1$2/>', $str);
// fix <buttononly.../> --> <button type="buttononly".../>
$str = preg_replace('#<buttononly\s(.*?)/>#u', '<button type="buttononly" $1/>', $str);
$str = preg_replace_callback(ADD_ET2_PREFIX_REGEXP, static function (array $matches)
{
return '<' . $matches[2] . 'et2-' . $matches[3] .
// web-components must not be self-closing (no "<et2-button .../>", but "<et2-button ...></et2-button>")
(substr($matches[4], -1) === '/' ? substr($matches[4], 0, -1) . '></et2-' . $matches[3] : $matches[4]) . '>';
}, $str);
$processing = microtime(true);
if(isset($cache) && (file_exists($cache_dir = dirname($cache)) || mkdir($cache_dir, 0755, true)))
{
file_put_contents($cache, $str);
}
}
// stop here for not existing file path-traversal for both file and cache here
if(empty($str) || strpos($path, '..') !== false)
{
http_response_code(404);
exit;
}
// headers to allow caching, egw_framework specifies etag on url to force reload, even with Expires header
Api\Session::cache_control(86400); // cache for one day
$etag = '"' . md5($str) . '"';
Header('ETag: ' . $etag);
// if servers send a If-None-Match header, response with 304 Not Modified, if etag matches
if(isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
{
header("HTTP/1.1 304 Not Modified");
exit;
}
// we run our own gzip compression, to set a correct Content-Length of the encoded content
if(function_exists('gzencode') && in_array('gzip', explode(',', $_SERVER['HTTP_ACCEPT_ENCODING']), true))
{
$gzip_start = microtime(true);
$str = gzencode($str);
header('Content-Encoding: gzip');
$gziping = microtime(true) - $gzip_start;
}
header('X-Timing: header-include=' . number_format($header_include - $GLOBALS['start'], 3) .
(empty($processing) ? ', cache-read=' . number_format($cache_read - $header_include, 3) :
', processing=' . number_format($processing - $header_include, 3)) .
(!empty($gziping) ? ', gziping=' . number_format($gziping, 3) : '') .
', total=' . number_format(microtime(true) - $GLOBALS['start'], 3)
);
// Content-Length header is important, otherwise browsers dont cache!
Header('Content-Length: ' . bytes($str));
echo $str;
exit; // stop further processing eg. redirect to login
}

View File

@ -0,0 +1,130 @@
/**
* EGroupware eTemplate2 - Box widget
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html, LitElement} from "@lion/core";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
export class Et2Box extends Et2Widget(LitElement) implements et2_IDetachedDOM
{
static get styles()
{
return [
...super.styles,
css`
:host {
display: block;
}
:host > div {
display: flex;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: stretch;
}
/* CSS for child elements */
::slotted(*) {
margin: 0px 2px;
flex: 1 0 auto;
}
::slotted([align="left"]) {
margin-right: auto;
order: -1;
}
::slotted([align="right"]) {
margin-left: auto;
order: 1;
}
`,
];
}
render()
{
return html`
<div ${this.id ? html`id="${this.id}"` : ''}>
<slot></slot>
</div> `;
}
set_label(new_label)
{
// Boxes don't have labels
}
_createNamespace() : boolean
{
return true;
}
/**
* Code for implementing et2_IDetachedDOM
*
* Individual widgets are detected and handled by the grid, but the interface is needed for this to happen
*
* @param {array} _attrs array to add further attributes to
*/
getDetachedAttributes(_attrs)
{
_attrs.push('data');
}
getDetachedNodes()
{
return [this.getDOMNode()];
}
setDetachedAttributes(_nodes, _values)
{
if(_values.data)
{
var pairs = _values.data.split(/,/g);
for(var i = 0; i < pairs.length; ++i)
{
var name_value = pairs[i].split(':');
jQuery(_nodes[0]).attr('data-' + name_value[0], name_value[1]);
}
}
}
}
customElements.define("et2-box", Et2Box);
export class Et2HBox extends Et2Box
{
static get styles()
{
return [
...super.styles,
css`
:host > div {
flex-direction: row;
}`
];
}
}
customElements.define("et2-hbox", Et2HBox);
export class Et2VBox extends Et2Box
{
static get styles()
{
return [
...super.styles,
css`
:host > div {
flex-direction: column;
}`
];
}
}
customElements.define("et2-vbox", Et2VBox);

View File

@ -0,0 +1,32 @@
/**
* Test file for Etemplate webComponent base widget Et2Box
*/
import {assert, fixture} from '@open-wc/testing';
import {Et2Box} from "../Et2Box";
import {html} from "lit-element";
describe("Box widget", () =>
{
// Reference to component under test
let element : Et2Box;
// Setup run before each test
beforeEach(async() =>
{
// Create an element to test with, and wait until it's ready
element = await fixture<Et2Box>(html`
<et2-box></et2-box>
`);
});
it('is defined', () =>
{
assert.instanceOf(element, Et2Box);
});
it('has no label', () =>
{
element.set_label("Nope");
assert.isEmpty(element.shadowRoot.querySelectorAll('.et2_label'));
})
});

View File

@ -0,0 +1,292 @@
/**
* EGroupware eTemplate2 - Button widget
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html} from "@lion/core";
import {LionButton} from "@lion/button";
import {SlotMixin} from "@lion/core";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
export class Et2Button extends Et2InputWidget(SlotMixin(LionButton))
{
protected _created_icon_node : HTMLImageElement;
protected clicked : boolean = false;
private _image : string;
/**
* images to be used as background-image, if none is explicitly applied and id matches given regular expression
*/
static readonly default_background_images : object = {
save: /save(&|\]|$)/,
apply: /apply(&|\]|$)/,
cancel: /cancel(&|\]|$)/,
delete: /delete(&|\]|$)/,
discard: /discard(&|\]|$)/,
edit: /edit(&|\[\]|$)/,
next: /(next|continue)(&|\]|$)/,
finish: /finish(&|\]|$)/,
back: /(back|previous)(&|\]|$)/,
copy: /copy(&|\]|$)/,
more: /more(&|\]|$)/,
check: /(yes|check)(&|\]|$)/,
cancelled: /no(&|\]|$)/,
ok: /ok(&|\]|$)/,
close: /close(&|\]|$)/,
add: /(add(&|\]|$)|create)/ // customfields use create*
};
/**
* Classnames added automatically to buttons to set certain hover background colors
*/
static readonly default_classes : object = {
et2_button_cancel: /cancel(&|\]|$)/, // yellow
et2_button_question: /(yes|no)(&|\]|$)/, // yellow
et2_button_delete: /delete(&|\]|$)/ // red
};
static get styles()
{
return [
...super.styles,
css`
:host {
padding: 1px 8px;
/* These should probably come from somewhere else */
border-radius: 3px;
background-color: #e6e6e6;
max-width: 125px;
}
:host([readonly]) {
display: none;
}
/* Set size for icon */
::slotted([slot="icon"][src]) {
width: 20px;
padding-right: 3px;
}
::slotted([slot="icon"][src='']) {
display: none;
}
`,
];
}
static get properties()
{
return {
...super.properties,
image: {type: String}
}
}
get slots()
{
return {
...super.slots,
icon: () =>
{
return document.createElement("img");
}
}
}
constructor()
{
super();
// Property default values
this._image = '';
// Do not add icon here, no children can be added in constructor
}
connectedCallback()
{
super.connectedCallback();
//this.classList.add("et2_button")
}
set image(new_image : string)
{
let oldValue = this._image;
if(new_image.indexOf("http") >= 0)
{
this._image = new_image
}
else
{
this._image = this.egw().image(new_image, 'etemplate');
}
this.requestUpdate("image", oldValue);
}
_handleClick(event : MouseEvent) : boolean
{
// ignore click on readonly button
if(this.disabled || this.readonly)
{
return false;
}
this.clicked = true;
// Cancel buttons don't trigger the close confirmation prompt
if(this.classList.contains("et2_button_cancel"))
{
this.getInstanceManager()?.skip_close_prompt();
}
if(!super._handleClick(event))
{
this.clicked = false;
return false;
}
// Submit the form
if(this.getType() !== "buttononly")
{
return this.getInstanceManager().submit();
}
this.clicked = false;
this.getInstanceManager()?.skip_close_prompt(false);
return true;
}
/**
* Handle changes that have to happen based on changes to properties
*
*/
requestUpdate(name : PropertyKey, oldValue)
{
super.requestUpdate(name, oldValue);
// Default image & class are determined based on ID
if(name == "id" && this._widget_id)
{
// Check against current value to avoid triggering another update
if(!this.image)
{
let image = this._get_default_image(this._widget_id);
if(image != this._image)
{
this.image = image;
}
}
let default_class = this._get_default_class(this._widget_id);
if(default_class && !this.classList.contains(default_class))
{
this.classList.add(default_class);
}
}
}
render()
{
if(this.readonly)
{
return '';
}
this._iconNode.src = this._image;
return html`
<div class="button-content et2_button" id="${this._buttonId}">
<slot name="icon"></slot>
<slot>${this._label}</slot>
</div> `;
}
/**
* Get a default image for the button based on ID
*
* @param {string} check_id
*/
_get_default_image(check_id : string) : string
{
if(!check_id)
{
return "";
}
if(typeof this.image == 'undefined')
{
for(const image in Et2Button.default_background_images)
{
if(check_id.match(Et2Button.default_background_images[image]))
{
return image;
}
}
}
return "";
}
/**
* Get a default class for the button based on ID
*
* @param check_id
* @returns {string}
*/
_get_default_class(check_id)
{
if(!check_id)
{
return "";
}
for(var name in Et2Button.default_classes)
{
if(check_id.match(Et2Button.default_classes[name]))
{
return name;
}
}
return "";
}
get _iconNode() : HTMLImageElement
{
return <HTMLImageElement>(Array.from(this.children)).find(
el => (<HTMLElement>el).slot === "icon",
);
}
/**
* Implementation of the et2_IInput interface
*/
/**
* Always return false as a button is never dirty
*/
isDirty()
{
return false;
}
resetDirty()
{
}
getValue()
{
if(this.clicked)
{
return true;
}
// If "null" is returned, the result is not added to the submitted
// array.
return null;
}
}
// @ts-ignore TypeScript is not recognizing that Et2Button is a LitElement
customElements.define("et2-button", Et2Button);

View File

@ -0,0 +1,69 @@
/**
* Test file for Etemplate webComponent base widget Et2Box
*/
import {assert, fixture} from '@open-wc/testing';
import {Et2Button} from "../Et2Button";
import type {Et2Widget} from "../../Et2Widget/Et2Widget";
import {html} from "lit-element";
import * as sinon from 'sinon';
describe("Button widget", () =>
{
// Reference to component under test
let element : Et2Button;
// Setup run before each test
beforeEach(async() =>
{
// Create an element to test with, and wait until it's ready
element = await fixture<Et2Button>(html`
<et2-button label="I'm a button"></et2-button>
`);
// Stub egw()
sinon.stub(element, "egw").returns({
tooltipUnbind: () => {},
// Image always give check mark. Use data URL to avoid having to serve an actual image
image: i => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo="
});
});
// Make sure it works
it('is defined', () =>
{
assert.instanceOf(element, Et2Button);
});
it('has a label', () =>
{
element.set_label("Label set");
assert.equal(element.textContent, "Label set");
})
it("click happens", () =>
{
// Setup
let clickSpy = sinon.spy();
element.onclick = clickSpy;
// Click
element.dispatchEvent(new MouseEvent("click"));
// Check for once & only once
assert(clickSpy.calledOnce, "Click only once");
})
it("gets an icon", async() =>
{
element.image = "check";
// Wait for the render to finish
await element.updateComplete;
let image = element.querySelectorAll("img");
assert.equal(image.length, 1);
assert.equal(image[0].src, element.egw().image("check"));
})
});

View File

@ -0,0 +1,151 @@
/**
* EGroupware eTemplate2 - Colorpicker widget (WebComponent)
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Hadi Nategh
*/
import {css, html, SlotMixin, render, RenderOptions} from "@lion/core";
import {LionInput} from "@lion/input";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
export class Et2Colorpicker extends Et2InputWidget(Et2Widget(SlotMixin(LionInput)))
{
private cleared : boolean = true;
static get styles()
{
return [
...super.styles,
css`
:host {
display: flex;
}
.input-group__suffix{
height: 12px;
}
.input-group__container {
align-items: center
}
:host(:hover) ::slotted([slot="suffix"]) {
width: 12px;
height: 12px;
display: inline-flex;
background-image: url(pixelegg/images/close.svg);
background-size: 10px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
`,
];
}
get slots()
{
return {
...super.slots,
input: () => this.__getInputNode(),
suffix: () => this.__getClearButtonNode()
}
}
constructor()
{
super();
// Override the default type of "text"
this.type = 'color';
// Bind the handlers, since slots bind without this as context
this._handleChange = this._handleChange.bind(this);
this._handleClickClear = this._handleClickClear.bind(this);
}
connectedCallback()
{
super.connectedCallback();
}
__getInputNode()
{
const renderParent = document.createElement('div');
render(
this._inputTemplate(),
renderParent,
<RenderOptions>({
scopeName: this.localName,
eventContext: this,
}),
);
return renderParent.firstElementChild;
}
_inputTemplate()
{
return html`
<input type="color" onchange="${this._handleChange}"/>
`;
}
/**
* Get the clear button node
* @returns {Element|null}
*/
__getClearButtonNode()
{
const renderParent = document.createElement('div');
render(
this._clearButtonTemplate(),
renderParent,
<RenderOptions>({
scopeName: this.localName,
eventContext: this,
}),
);
return renderParent.firstElementChild;
}
_clearButtonTemplate()
{
return html`
<span class="clear-icon" @click="${this._handleClickClear}"></span>
`;
}
_handleChange(e)
{
this.set_value(e.target.value);
}
_handleClickClear()
{
this.set_value('');
}
getValue()
{
let value = this._inputNode.value;
if (this.cleared || value === '#FFFFFF' || value === '#ffffff') {
return '';
}
return value;
}
set_value(color)
{
if(!color)
{
color = '';
}
this.cleared = !color;
this._inputNode.value = color;
}
}
customElements.define('et2-colorpicker', Et2Colorpicker);

View File

@ -0,0 +1,38 @@
/**
* Test file for Etemplate webComponent base widget Et2Colorpicker
*/
import {assert, fixture} from '@open-wc/testing';
import {Et2Colorpicker} from "../Et2Colorpicker";
import {html} from "lit-element";
describe("Colorpicker widget", () =>
{
// Reference to component under test
let element : Et2Colorpicker;
// Setup run before each test
beforeEach(async() =>
{
// Create an element to test with, and wait until it's ready
element = await fixture<Et2Colorpicker>(html`
<et2-colorpicker></et2-colorpicker>
`);
});
it('is defined', () =>
{
assert.instanceOf(element, Et2Colorpicker);
});
it('clearing value', () =>
{
// set a value
element.set_value("11111");
// trigger the clear button
element.__getClearButtonNode().dispatchEvent(new MouseEvent('click'));
assert.equal(element.getValue(), "");
});
});

View File

@ -0,0 +1,441 @@
/**
* EGroupware eTemplate2 - Date widget (WebComponent)
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html} from "@lion/core";
import {LionInputDatepicker} from "@lion/input-datepicker";
import {Unparseable} from "@lion/form-core";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
/**
* Parse a date string into a Date object
* Time will be 00:00:00 UTC
*
* @param {string} dateString
* @returns {Date | undefined}
*/
export function parseDate(dateString)
{
// First try the server format
if(dateString.substr(-1) === "Z")
{
try
{
let date = new Date(dateString);
if(date instanceof Date)
{
return date;
}
}
catch(e)
{
// Nope, that didn't parse directly
}
}
let formatString = <string>(window.egw.preference("dateformat") || 'Y-m-d');
formatString = formatString.replaceAll(new RegExp('[-/\.]', 'ig'), '-');
let parsedString = "";
switch(formatString)
{
case 'd-m-Y':
parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
break;
case 'm-d-Y':
parsedString = `${dateString.slice(6, 10)}/${dateString.slice(0, 2,)}/${dateString.slice(3, 5)}`;
break;
case 'Y-m-d':
parsedString = `${dateString.slice(0, 4)}/${dateString.slice(5, 7,)}/${dateString.slice(8, 10)}`;
break;
case 'Y-d-m':
parsedString = `${dateString.slice(0, 4)}/${dateString.slice(8, 10)}/${dateString.slice(5, 7)}`;
break;
case 'd-M-Y':
parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
break;
default:
parsedString = '0000/00/00';
}
const [year, month, day] = parsedString.split('/').map(Number);
const parsedDate = new Date(`${year}-${month < 10 ? "0" + month : month}-${day < 10 ? "0" + day : day}T00:00:00Z`);
// Check if parsedDate is not `Invalid Date` or that the date has changed (e.g. the not existing 31.02.2020)
if(
year > 0 &&
month > 0 &&
day > 0 &&
parsedDate.getUTCDate() === day &&
parsedDate.getUTCMonth() === month - 1
)
{
return parsedDate;
}
return undefined;
}
/**
* To parse a time into a Date object
* Date will be 1970-01-01, time is in UTC to avoid browser issues
*
* @param {string} timeString
* @returns {Date | undefined}
*/
export function parseTime(timeString)
{
// First try the server format
if(timeString.substr(-1) === "Z")
{
try
{
let date = new Date(timeString);
if(date instanceof Date)
{
return date;
}
}
catch(e)
{
// Nope, that didn't parse directly
}
}
let am_pm = timeString.endsWith("pm") || timeString.endsWith("PM") ? 12 : 0;
let strippedString = timeString.replaceAll(/[^0-9:]/gi, '');
if(timeString.startsWith("12") && strippedString != timeString)
{
// 12:xx am -> 0:xx, 12:xx pm -> 12:xx
am_pm -= 12;
}
const [hour, minute] = strippedString.split(':').map(Number);
const parsedDate = new Date("1970-01-01T00:00:00Z");
parsedDate.setUTCHours(hour + am_pm);
parsedDate.setUTCMinutes(minute);
// Check if parsedDate is not `Invalid Date` or that the time has changed
if(
parsedDate.getUTCHours() === hour + am_pm &&
parsedDate.getUTCMinutes() === minute
)
{
return parsedDate;
}
return undefined;
}
/**
* To parse a date+time into an object
* Time is in UTC to avoid browser issues
*
* @param {string} dateTimeString
* @returns {Date | undefined}
*/
export function parseDateTime(dateTimeString)
{
// First try some common invalid values
if(dateTimeString === "" || dateTimeString === "0" || dateTimeString === 0)
{
return undefined;
}
// Next try server format
if(typeof dateTimeString === "string" && dateTimeString.substr(-1) === "Z" || !isNaN(dateTimeString))
{
if(!isNaN(dateTimeString) && parseInt(dateTimeString) == dateTimeString)
{
this.egw().debug("warn", "Invalid date/time string: " + dateTimeString);
dateTimeString *= 1000;
}
try
{
let date = new Date(dateTimeString);
if(date instanceof Date)
{
return date;
}
}
catch(e)
{
// Nope, that didn't parse directly
}
}
const date = parseDate(dateTimeString);
let explody = dateTimeString.split(" ");
explody.shift();
const time = parseTime(explody.join(" "));
if(typeof date === "undefined" || typeof time === "undefined")
{
return undefined;
}
date.setUTCHours(time.getUTCHours());
date.setUTCMinutes(time.getUTCMinutes());
date.setUTCSeconds(time.getUTCSeconds());
return date;
}
/**
* Format dates according to user preference
*
* @param {Date} date
* @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
* set 'dateFormat': "Y-m-d" to specify a particular format
* @returns {string}
*/
export function formatDate(date : Date, options = {dateFormat: ""}) : string
{
if(!date || !(date instanceof Date))
{
return "";
}
let _value = '';
// Add timezone offset back in, or formatDate will lose those hours
let formatDate = new Date(date.valueOf() - date.getTimezoneOffset() * 60 * 1000);
let dateformat = options.dateFormat || <string>window.egw.preference("dateformat") || 'Y-m-d';
var replace_map = {
d: (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(),
m: (date.getUTCMonth() < 9 ? "0" : "") + (date.getUTCMonth() + 1),
Y: "" + date.getUTCFullYear()
}
var re = new RegExp(Object.keys(replace_map).join("|"), "gi");
_value = dateformat.replace(re, function(matched)
{
return replace_map[matched];
});
return _value;
}
/**
* Format dates according to user preference
*
* @param {Date} date
* @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
* set 'timeFormat': "12" to specify a particular format
* @returns {string}
*/
export function formatTime(date : Date, options = {timeFormat: ""}) : string
{
if(!date || !(date instanceof Date))
{
return "";
}
let _value = '';
let timeformat = options.timeFormat || <string>window.egw.preference("timeformat") || "24";
let hours = (timeformat == "12" && date.getUTCHours() > 12) ? (date.getUTCHours() - 12) : date.getUTCHours();
if(timeformat == "12" && hours == 0)
{
// 00:00 is 12:00 am
hours = 12;
}
_value = (timeformat == "24" && hours < 10 ? "0" : "") + hours + ":" +
(date.getUTCMinutes() < 10 ? "0" : "") + (date.getUTCMinutes()) +
(timeformat == "24" ? "" : (date.getUTCHours() < 12 ? " am" : " pm"));
return _value;
}
/**
* Format date+time according to user preference
*
* @param {Date} date
* @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
* set 'dateFormat': "Y-m-d", 'timeFormat': "12" to specify a particular format
* @returns {string}
*/
export function formatDateTime(date : Date, options = {dateFormat: "", timeFormat: ""}) : string
{
if(!date || !(date instanceof Date))
{
return "";
}
return formatDate(date, options) + " " + formatTime(date, options);
}
export class Et2Date extends Et2InputWidget(LionInputDatepicker)
{
static get styles()
{
return [
...super.styles,
css`
:host([focused]) ::slotted(button), :host(:hover) ::slotted(button) {
display: inline-block;
}
::slotted(.calendar_button) {
border: none;
background: transparent;
margin-left: -20px;
display: none;
}
`,
];
}
static get properties()
{
return {
...super.properties
}
}
constructor()
{
super();
this.parser = parseDate;
this.formatter = formatDate;
}
connectedCallback()
{
super.connectedCallback();
}
/**
* @param {Date} modelValue
*/
// eslint-disable-next-line class-methods-use-this
serializer(modelValue : Date)
{
// isValidDate() is hidden inside LionInputDate, and not exported
// @ts-ignore Can't call isNan(Date), but we're just checking
if(!(modelValue instanceof Date) || isNaN(modelValue))
{
return '';
}
// modelValue is localized, so we take the timezone offset in milliseconds and subtract it
// before converting it to ISO string.
const offset = modelValue.getTimezoneOffset() * 60000;
return new Date(modelValue.getTime() - offset).toJSON().replace(/\.\d{3}Z$/, 'Z');
}
set_value(value)
{
this.modelValue = this.parser(value);
}
getValue()
{
if(this.readOnly)
{
return null;
}
// The supplied value was not understandable, return null
if(this.modelValue instanceof Unparseable || !this.modelValue)
{
return null;
}
this.modelValue.setUTCHours(0);
this.modelValue.setUTCMinutes(0);
this.modelValue.setSeconds(0, 0);
return this.modelValue.toJSON();
}
get _overlayReferenceNode()
{
return this.getInputNode();
}
/**
* @override Configures OverlayMixin
* @desc overrides default configuration options for this component
* @returns {Object}
*/
_defineOverlayConfig()
{
this.hasArrow = false;
if(window.innerWidth >= 600)
{
return {
hidesOnOutsideClick: true,
placementMode: 'local',
popperConfig: {
placement: 'bottom-end',
},
};
}
return super.withBottomSheetConfig();
}
/**
* The LionCalendar shouldn't know anything about the modelValue;
* it can't handle Unparseable dates, but does handle 'undefined'
* @param {?} modelValue
* @returns {Date|undefined} a 'guarded' modelValue
*/
static __getSyncDownValue(modelValue)
{
if(!(modelValue instanceof Date))
{
return undefined;
}
const offset = modelValue.getTimezoneOffset() * 60000;
return new Date(modelValue.getTime() + offset);
}
/**
* Overriding from parent for read-only
*
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate()
{
if(this.readOnly)
{
return this.formattedValue;
}
else
{
return super._inputGroupInputTemplate();
}
}
/**
* Overriding parent to add class to button, and use an image instead of unicode emoji
*/
// eslint-disable-next-line class-methods-use-this
_invokerTemplate()
{
if(this.readOnly)
{
return '';
}
let img = this.egw() ? this.egw().image("calendar") || '' : '';
return html`
<button
type="button"
class="calendar_button"
@click="${this.__openCalendarOverlay}"
id="${this.__invokerId}"
aria-label="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
title="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
>
<img src="${img}" style="width:16px"/>
</button>
`;
}
}
// @ts-ignore TypeScript is not recognizing that Et2Date is a LitElement
customElements.define("et2-date", Et2Date);

View File

@ -0,0 +1,69 @@
/**
* EGroupware eTemplate2 - Date+Time widget (WebComponent)
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html} from "@lion/core";
import {Et2Date, formatDateTime, parseDateTime} from "./Et2Date";
import {Unparseable} from "@lion/form-core";
export class Et2DateTime extends Et2Date
{
static get styles()
{
return [
...super.styles,
css`
:host([focused]) ::slotted(button), :host(:hover) ::slotted(button) {
display: inline-block;
}
::slotted(.calendar_button) {
border: none;
background: transparent;
margin-left: -20px;
display: none;
}
`,
];
}
static get properties()
{
return {
...super.properties
}
}
constructor()
{
super();
this.parser = parseDateTime;
this.formatter = formatDateTime;
}
getValue()
{
if(this.readOnly)
{
return null;
}
// The supplied value was not understandable, return null
if(this.modelValue instanceof Unparseable || !this.modelValue)
{
return null;
}
return this.modelValue.toJSON();
}
}
// @ts-ignore TypeScript is not recognizing that Et2DateTime is a LitElement
customElements.define("et2-datetime", Et2DateTime);

View File

@ -0,0 +1,125 @@
/**
* Test file for Etemplate webComponent Date
*/
import {assert, elementUpdated, fixture} from '@open-wc/testing';
import {Et2Date} from "../Et2Date";
import {html} from "lit-element";
import * as sinon from 'sinon';
describe("Date widget", () =>
{
// Reference to component under test
let element : Et2Date;
// Setup run before each test
beforeEach(async() =>
{
// Create an element to test with, and wait until it's ready
// @ts-ignore
element = await fixture<Et2Date>(html`
<et2-date label="I'm a date"></et2-date>
`);
// Stub egw()
sinon.stub(element, "egw").returns({
tooltipUnbind: () => {},
// Image always give check mark. Use data URL to avoid having to serve an actual image
image: i => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo="
});
// Stub global egw for preference
// @ts-ignore
window.egw = {
preference: () => 'Y-m-d'
};
});
// Make sure it works
it('is defined', () =>
{
assert.instanceOf(element, Et2Date);
});
it('has a label', () =>
{
element.set_label("Label set");
assert.equal(element.querySelector("[slot='label']").textContent, "Label set");
})
it('Readonly does not return a value', async() =>
{
element.readOnly = true;
let test_time_string = '2008-09-22T12:00:00.000Z';
element.set_value(test_time_string);
// wait for asychronous changes to the DOM
await elementUpdated(element);
// Read-only widget returns null
assert.equal(element.getValue(), null);
});
it('No value shows no value', () =>
{
assert.equal(element.querySelector("input").textContent, "");
assert.equal(element.get_value(), null);
});
it("'0' shows no value", async() =>
{
element.set_value("0");
// wait for asychronous changes to the DOM
await elementUpdated(element);
assert.equal(element.querySelector("input").value, "");
assert.equal(element.get_value(), null);
});
const tz_list = [
{name: "America/Edmonton", offset: -600},
{name: "UTC", offset: 0},
{name: "Australia/Adelaide", offset: 630}
];
for(let tz of tz_list)
{
describe("Timezone: " + tz.name, () =>
{
// TODO: Figure out how to mock timezone...
// Stub timezone offset to return a different value
let tz_offset_stub = sinon.stub(Date.prototype, "getTimezoneOffset").returns(
tz.offset
);
let test_time_string = '2008-09-22T12:00:00.000Z';
let test_time = new Date(test_time_string);
it('Can accept a value', async() =>
{
element.set_value(test_time_string);
// wait for asychronous changes to the DOM
await elementUpdated(element);
// Widget gives time as a string so we can send to server, but zeros the time
assert.equal(element.getValue().substr(0, 11), test_time_string.substr(0, 11));
});
/* Doesn't work yet
it("Can be modified", () =>
{
element.getInputNode().value = "2008-09-22";
let event = new Event("change");
element.getInputNode().dispatchEvent(event);
// Use a Promise to wait for asychronous changes to the DOM
return Promise.resolve().then(() =>
{
assert.equal(element.getValue(), "2008-09-22T00:00:00.000Z");
});
});
*/
// Put timezone offset back
tz_offset_stub.restore();
});
}
});

View File

@ -0,0 +1,125 @@
/**
* Test file for Etemplate webComponent Date
*/
import {assert, elementUpdated, fixture, oneEvent} from '@open-wc/testing';
import {html} from "lit-element";
import * as sinon from 'sinon';
import {Et2DateTime} from "../Et2DateTime";
describe("DateTime widget", () =>
{
// Reference to component under test
let element : Et2DateTime;
// Setup run before each test
beforeEach(async() =>
{
// Create an element to test with, and wait until it's ready
// @ts-ignore
element = await fixture<Et2DateTime>(html`
<et2-datetime label="I'm a date-time"></et2-datetime>
`);
// Stub egw()
sinon.stub(element, "egw").returns({
tooltipUnbind: () => {},
// Image always give check mark. Use data URL to avoid having to serve an actual image
image: i => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo="
});
// Stub global egw for preference
// @ts-ignore
window.egw = {
preference: () => 'Y-m-d'
};
});
// Make sure it works
it('is defined', () =>
{
assert.instanceOf(element, Et2DateTime);
});
it('has a label', () =>
{
element.set_label("Label set");
assert.equal(element.querySelector("[slot='label']").textContent, "Label set");
})
it('Readonly does not return a value', async() =>
{
element.readOnly = true;
let test_time_string = '2008-09-22T12:00:00.000Z';
element.set_value(test_time_string);
// wait for asychronous changes to the DOM
await elementUpdated(<Element><unknown>element);
// Read-only widget returns null
assert.equal(element.getValue(), null);
});
it('No value shows no value', () =>
{
assert.equal(element.querySelector("input").textContent, "");
assert.equal(element.get_value(), null);
});
it("'0' shows no value", async() =>
{
element.set_value("0");
// wait for asychronous changes to the DOM
await elementUpdated(element);
assert.equal(element.querySelector("input").value, "");
assert.equal(element.get_value(), null);
});
const tz_list = [
{name: "America/Edmonton", offset: -600},
{name: "UTC", offset: 0},
{name: "Australia/Adelaide", offset: 630}
];
for(let tz of tz_list)
{
describe("Timezone: " + tz.name, () =>
{
// TODO: Figure out how to mock timezone...
// Stub timezone offset to return a different value
let tz_offset_stub = sinon.stub(Date.prototype, "getTimezoneOffset").returns(
tz.offset
);
let test_time_string = '2008-09-22T12:00:00.000Z';
let test_time = new Date(test_time_string);
it('Can accept a value', async() =>
{
element.set_value(test_time_string);
// wait for asychronous changes to the DOM
await elementUpdated(element);
// Widget gives time as a string so we can send to server, but zeros the time
assert.equal(element.getValue().substr(0, 11), test_time_string.substr(0, 11));
});
/* Doesn't work yet
it("Can be modified", () =>
{
element.getInputNode().value = "2008-09-22";
let event = new Event("change");
element.getInputNode().dispatchEvent(event);
// Use a Promise to wait for asychronous changes to the DOM
return Promise.resolve().then(() =>
{
assert.equal(element.getValue(), "2008-09-22T00:00:00.000Z");
});
});
*/
// Put timezone offset back
tz_offset_stub.restore();
});
}
});

View File

@ -0,0 +1,100 @@
/**
* Test file for Etemplate date formatting
*
* For now, the best way to check if different timezones work is to change the TZ of your
* computer, then run the tests,
*/
import {assert} from '@open-wc/testing';
import {formatDate, formatTime} from "../Et2Date";
describe("Date formatting", () =>
{
// Function under test
let formatter = formatDate;
// Setup run before each test
beforeEach(async() =>
{
// Stub global egw for preference
// @ts-ignore
window.egw = {
preference: () => 'Y-m-d'
};
});
it("Handles Y-m-d", () =>
{
let test_string = '2021-09-22';
let test_date = new Date("2021-09-22T12:34:56Z");
let formatted = formatter(test_date);
assert.equal(formatted, test_string);
});
it("Handles Y.d.m", () =>
{
let test_string = '2021.22.09';
let test_date = new Date("2021-09-22T12:34:56Z");
//@ts-ignore
window.egw = {
preference: () => 'Y.d.m'
};
let formatted = formatter(test_date);
assert.equal(formatted, test_string);
});
});
describe("Time formatting", () =>
{
// Function under test
let formatter = formatTime;
// Setup run before each test
beforeEach(async() =>
{
// Stub global egw for preference
// @ts-ignore
window.egw = {
preference: () => 'Y-m-d'
};
});
it("Handles 12h", () =>
{
const test_data = {
"9:15 am": new Date('2021-09-22T09:15:00Z'),
"12:00 am": new Date('2021-09-22T00:00:00Z'),
"12:00 pm": new Date('2021-09-22T12:00:00Z'),
"5:00 pm": new Date('2021-09-22T17:00:00Z'),
};
for(let test_string of Object.keys(test_data))
{
let test_date = test_data[test_string];
let formatted = formatter(test_date, {timeFormat: "12"});
assert.equal(formatted, test_string);
}
});
it("Handles 24h", () =>
{
const test_data = {
"09:15": new Date('2021-09-22T09:15:00Z'),
"00:00": new Date('2021-09-22T00:00:00Z'),
"12:00": new Date('2021-09-22T12:00:00Z'),
"17:00": new Date('2021-09-22T17:00:00Z'),
};
for(let test_string of Object.keys(test_data))
{
let test_date = test_data[test_string];
let formatted = formatter(test_date, {timeFormat: "24"});
assert.equal(formatted, test_string);
}
});
});

View File

@ -0,0 +1,150 @@
/**
* Test file for Etemplate date parsing
*/
import {assert} from '@open-wc/testing';
import {parseDate, parseTime} from "../Et2Date";
describe("Date parsing", () =>
{
// Function under test
let parser = parseDate;
// Setup run before each test
beforeEach(async() =>
{
// Stub global egw for preference
// @ts-ignore
window.egw = {
preference: () => 'Y-m-d'
};
});
it("Handles server format", () =>
{
let test_string = '2021-09-22T19:22:00Z';
let test_date = new Date(test_string);
let parsed = parser(test_string);
// Can't compare results - different objects
//assert.equal(parsed, test_date);
assert.equal(parsed.toJSON(), test_date.toJSON());
});
it("Handles Y-m-d", () =>
{
let test_string = '2021-09-22';
let test_date = new Date("2021-09-22T00:00:00Z");
let parsed = parser(test_string);
assert.equal(parsed.toJSON(), test_date.toJSON());
});
it("Handles Y.d.m", () =>
{
let test_string = '2021.22.09';
let test_date = new Date("2021-09-22T00:00:00Z");
//@ts-ignore
window.egw = {
preference: () => 'Y.d.m'
};
let parsed = parser(test_string);
assert.equal(parsed.toJSON(), test_date.toJSON());
});
it("Handles '0'", () =>
{
let test_string = '0';
let test_date = undefined;
//@ts-ignore
window.egw = {
preference: () => 'Y.d.m'
};
let parsed = parser(test_string);
assert.equal(parsed, test_date);
});
});
describe("Time parsing", () =>
{
// Setup run before each test
beforeEach(async() =>
{
// Stub global egw for preference
// @ts-ignore
window.egw = {
preference: () => 'Y-m-d'
};
});
it("Handles 12h", () =>
{
const test_data = {
// As expected
"9:15 am": new Date('1970-01-01T09:15:00Z'),
"12:00 am": new Date('1970-01-01T00:00:00Z'),
"12:00 pm": new Date('1970-01-01T12:00:00Z'),
"5:00 pm": new Date('1970-01-01T17:00:00Z'),
"11:59 pm": new Date('1970-01-01T23:59:00Z'),
// Not valid, should be undefined
"invalid": undefined,
"23:45 pm": undefined,
"0": undefined,
"": undefined
};
for(let test_string of Object.keys(test_data))
{
let test_date = test_data[test_string];
let parsed = parseTime(test_string);
if(typeof test_date == "undefined")
{
assert.isUndefined(parsed);
}
else
{
assert.equal(parsed.toJSON(), test_date.toJSON());
}
}
});
it("Handles 24h", () =>
{
const test_data = {
"09:15": new Date('1970-01-01T09:15:00Z'),
"00:00": new Date('1970-01-01T00:00:00Z'),
"12:00": new Date('1970-01-01T12:00:00Z'),
"17:00": new Date('1970-01-01T17:00:00Z'),
"23:59": new Date('1970-01-01T23:59:00Z'),
// Not valid, should be undefined
"invalid": undefined,
"23:45 pm": undefined,
"0": undefined,
"": undefined
};
for(let test_string of Object.keys(test_data))
{
let test_date = test_data[test_string];
let parsed = parseTime(test_string);
if(typeof test_date == "undefined")
{
assert.isUndefined(parsed);
}
else
{
assert.equal(parsed.toJSON(), test_date.toJSON());
}
}
});
});

View File

@ -0,0 +1,164 @@
/**
* EGroupware eTemplate2 - Iframe widget (WebComponent)
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Hadi Nategh
*/
import {css, html, LitElement, SlotMixin} from "@lion/core";
import {Et2Widget} from "../Et2Widget/Et2Widget";
export class Et2Iframe extends Et2Widget(SlotMixin(LitElement))
{
static get styles()
{
return [
...super.styles,
css`
:host {
display: flex;
}
:host > iframe {
width: 100%;
height: 100%;
}
/* Custom CSS */
`,
];
}
static get properties()
{
return {
...super.properties,
label: {type: String},
seamless: {type: Boolean},
name: {type: String},
fullscreen: {type: Boolean},
needed: {type: Boolean},
src: {type:String},
allow: {type: String}
}
}
constructor(...args : any[])
{
super(...args);
}
get slots()
{
return {
...super.slots
};
}
connectedCallback()
{
super.connectedCallback();
}
render() {
return html`
<iframe ${this.id ? html`id="${this.id}"` : ''} allowfullscreen="${this.fullscreen}" seamless="${this.seamless}" name="${this.name}" allow="${this.allow}"></iframe>
<slot>${this._label}</slot>
`;
}
__getIframeNode()
{
return this.shadowRoot.querySelector('iframe');
}
/**
* Set the URL for the iframe
*
* Sets the src attribute to the given value
*
* @param _value String URL
*/
set_src(_value)
{
if(_value.trim() != "")
{
if(_value.trim() == 'about:blank')
{
this.__getIframeNode().src = _value;
}
else
{
// Load the new page, but display a loader
let loader = jQuery('<div class="et2_iframe loading"/>');
this.__getIframeNode().before(loader);
window.setTimeout(function() {
this.__getIframeNode().src = _value;
this.__getIframeNode().addEventListener('load',function() {
loader.remove();
});
}.bind(this),0);
}
}
}
/**
* Set name of iframe (to be used as target for links)
*
* @param _name
*/
set_name(_name)
{
this.options.name = _name;
this.__getIframeNode().attribute('name', _name);
}
set_allow (_allow)
{
this.options.allow = _allow;
this.__getIframeNode().attribute('allow', _allow);
}
/**
* Make it look like part of the containing document
*
* @param _seamless boolean
*/
set_seamless(_seamless)
{
this.options.seamless = _seamless;
this.__getIframeNode().attribute("seamless", _seamless);
}
set_value(_value)
{
if(typeof _value == "undefined") _value = "";
if(_value.trim().indexOf("http") == 0 || _value.indexOf('about:') == 0 || _value[0] == '/')
{
// Value is a URL
this.set_src(_value);
}
else
{
// Value is content
this.set_srcdoc(_value);
}
}
/**
* Sets the content of the iframe
*
* Sets the srcdoc attribute to the given value
*
* @param _value String Content of a document
*/
set_srcdoc(_value)
{
this.__getIframeNode().attribute("srcdoc", _value);
}
}
// @ts-ignore TypeScript is not recognizing that Et2Iframe is a LitElement
customElements.define("et2-iframe", Et2Iframe);

View File

@ -0,0 +1,156 @@
import {et2_IInput, et2_IInputNode} from "../et2_core_interfaces";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {dedupeMixin} from "@lion/core";
/**
* This mixin will allow any LitElement to become an Et2InputWidget
*
* Usage:
* export class Et2Button extends Et2InputWidget(LitWidget)) {...}
*/
/**
* Need to define the interface first, to get around TypeScript issues with protected/public
* This must match the public API for Et2InputWidgetClass
* @see https://lit.dev/docs/composition/mixins/#typing-the-subclass
*/
export declare class Et2InputWidgetInterface
{
readOnly : boolean;
protected value : string | number | Object;
public set_value(any) : void;
public get_value() : any;
public getValue() : any;
public isDirty() : boolean;
public resetDirty() : void;
public isValid(messages : string[]) : boolean;
}
const Et2InputWidgetMixin = (superclass) =>
{
class Et2InputWidgetClass extends Et2Widget(superclass) implements et2_IInput, et2_IInputNode
{
protected value : string | number | Object;
protected _oldValue : string | number | Object;
protected node : HTMLElement;
/** WebComponent **/
static get styles()
{
return [
...super.styles
];
}
static get properties()
{
return {
...super.properties,
// readOnly is what the property is in Lion, readonly is the attribute
readOnly: {
type: Boolean,
attribute: 'readonly',
reflect: true,
},
// readonly is what is in the templates
// I put this in here so loadWebComponent finds it when it tries to set it from the template
readonly: {
type: Boolean
}
};
}
constructor(...args : any[])
{
super(...args);
}
connectedCallback()
{
super.connectedCallback();
this.node = this.getInputNode();
}
set_value(new_value)
{
this.value = new_value;
}
get_value()
{
return this.getValue();
}
getValue()
{
return typeof this.serializedValue !== "undefined" ? this.serializedValue : this.modalValue;
}
isDirty()
{
let value = this.getValue();
if(typeof value !== typeof this._oldValue)
{
return true;
}
if(this._oldValue === value)
{
return false;
}
switch(typeof this._oldValue)
{
case "object":
if(Array.isArray(this._oldValue) &&
this._oldValue.length !== value.length
)
{
return true;
}
for(let key in this._oldValue)
{
if(this._oldValue[key] !== value[key])
{
return true;
}
}
return false;
default:
return this._oldValue != value;
}
}
resetDirty()
{
this._oldValue = this.getValue();
}
isValid(messages)
{
var ok = true;
// Check for required
if(this.options && this.options.needed && !this.options.readonly && !this.disabled &&
(this.getValue() == null || this.getValue().valueOf() == ''))
{
messages.push(this.egw().lang('Field must not be empty !!!'));
ok = false;
}
return ok;
}
getInputNode()
{
// From LionInput
return this._inputNode;
}
}
return Et2InputWidgetClass;
}
export const Et2InputWidget = dedupeMixin(Et2InputWidgetMixin);

View File

@ -0,0 +1,39 @@
/**
* Common base for easily running some standard tests on all input widgets
*
* This file should not get run on its own, extend it
*
* TODO: Not sure exactly how to make this happen yet. Maybe:
* https://github.com/mochajs/mocha/wiki/Shared-Behaviours
* <code>
* shared:
* exports.shouldBehaveLikeAUser = function() {
* it('should have .name.first', function() {
* this.user.name.first.should.equal('tobi');
* })
*
* it('should have .name.last', function() {
* this.user.name.last.should.equal('holowaychuk');
* })
*
* describe('.fullname()', function() {
* it('should return the full name', function() {
* this.user.fullname().should.equal('tobi holowaychuk');
* })
* })
* };
* test.js:
*
* var User = require('./user').User
* , Admin = require('./user').Admin
* , shared = require('./shared');
*
* describe('User', function() {
* beforeEach(function() {
* this.user = new User('tobi', 'holowaychuk');
* })
*
* shared.shouldBehaveLikeAUser();
* })
* </code>
*/

View File

@ -0,0 +1,128 @@
/**
* EGroupware eTemplate2 - Textbox widget (WebComponent)
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html} from "@lion/core";
import {LionTextarea} from "@lion/textarea";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {Et2Widget} from "../Et2Widget/Et2Widget";
export class Et2Textarea extends Et2InputWidget(LionTextarea)
{
static get styles()
{
return [
...super.styles,
css`
:host {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
/* Get text area to fill its space */
.form-field__group-two {
height: 100%;
display: flex;
flex-direction: column;
}
.input-group {
height: 100%;
display: flex;
}
.input-group__container {
width: 100%;
}
`,
];
}
static get properties()
{
return {
...super.properties,
/**
* Specify the width of the text area.
* If not set, it will expand to fill the space available.
*/
width: {type: String},
/**
* Specify the height of the text area.
* If not set, it will expand to fill the space available.
*/
height: {type: String},
onkeypress: Function,
}
}
constructor()
{
super();
this.rows = "";
}
connectedCallback()
{
super.connectedCallback();
if(this._width && this._inputNode)
{
this._inputNode.style.width = this._width;
}
if(this._height && this._inputNode)
{
this._inputNode.style.height = this._height;
}
}
/**
* Use width and height attributes to affect style
* It would be better to deprecate these and just use CSS
*
* @param value
*/
set width(value)
{
let oldValue = this._width;
this._width = value;
this.requestUpdate("width", oldValue);
}
set height(value)
{
let oldValue = this._height;
this._height = value;
this.requestUpdate("height", oldValue);
}
/** Override some parent stuff to get sizing how we like it **/
setTextareaMaxHeight()
{
this._inputNode.style.maxHeight = 'inherit';
}
__initializeAutoresize()
{
return;
}
__startAutoresize()
{
return;
}
}
// @ts-ignore TypeScript is not recognizing that Et2Textarea is a LitElement
customElements.define("et2-textarea", Et2Textarea);

View File

@ -0,0 +1,49 @@
/**
* EGroupware eTemplate2 - Textbox widget (WebComponent)
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html} from "@lion/core";
import {LionInput} from "@lion/input";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
export class Et2Textbox extends Et2InputWidget(LionInput)
{
static get styles()
{
return [
...super.styles,
css`
/* Custom CSS */
`,
];
}
static get properties()
{
return {
...super.properties,
onkeypress: Function,
}
}
constructor(...args : any[])
{
super(...args);
}
connectedCallback()
{
super.connectedCallback();
}
}
// @ts-ignore TypeScript is not recognizing that Et2Textbox is a LitElement
customElements.define("et2-textbox", Et2Textbox);

View File

@ -0,0 +1,32 @@
/**
* Test file for Etemplate webComponent Textbox
*/
import {assert, fixture} from '@open-wc/testing';
import {Et2Textbox} from "../Et2Textbox";
import {html} from "lit-element";
describe("Textbox widget", () =>
{
// Reference to component under test
let element : Et2Textbox;
// Setup run before each test
beforeEach(async() =>
{
// Create an element to test with, and wait until it's ready
element = await fixture<Et2Textbox>(html`
<et2-textbox></et2-textbox>
`);
});
it('is defined', () =>
{
assert.instanceOf(element, Et2Textbox);
});
it('has a label', () =>
{
element.set_label("Yay label");
assert.isEmpty(element.shadowRoot.querySelectorAll('.et2_label'));
})
});

View File

@ -0,0 +1,34 @@
/**
* Testing Et2Textbox input / values
*/
import {assert, fixture} from "@open-wc/testing";
import {html} from "lit-html";
import {Et2Textbox} from "../Et2Textbox";
describe("Textbox input / values", () =>
{
// Reference to component under test
let element : Et2Textbox;
// Setup run before each test
beforeEach(async() =>
{
// Create an element to test with, and wait until it's ready
element = await fixture<Et2Textbox>(html`
<et2-textbox></et2-textbox>
`);
});
it("Takes a value", () =>
{
/*
Complains about set_value() being missing?
let test_value = "test value";
debugger;
element.set_value(test_value);
assert(document.querySelector('input').should.have.text(test_value))
*/
})
});

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
/**
* EGroupware eTemplate2 - Button widget
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
/* Commented out while we work on rollup
import {LitElement,html} from "https://cdn.skypack.dev/lit-element";
import {SlButton} from "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.44/dist/shoelace.js";
export class Et2Button extends SlButton
{
size='small';
}
customElements.define("et2-button",Et2Button);
*/

View File

@ -238,7 +238,7 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
// Append this node at its index // Append this node at its index
var idx = this.getDOMIndex(); var idx = this.getDOMIndex();
if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) if(idx < 0 || idx > this.parentNode.childNodes.length - 1)
{ {
this.parentNode.appendChild(node); this.parentNode.appendChild(node);
} }
@ -283,9 +283,19 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
} }
} }
// _node is actually a Web Component // _node is actually a Web Component
else if (_node instanceof Element ) else if(_node instanceof Element)
{ {
this.getDOMNode().append(_node); if(this.getDOMNode(_node))
{
this.getDOMNode(_node).append(_node);
}
else
{
// Warn about it. This slows down loading, as it requires a second pass (loadingFinished) to get the child
// properly added.
console.warn("Legacy widget " + this.getType() + "[#" + this.options.id + "] could not handle adding a child (" +
_node.getType() + (_node.id ? "#" + _node.id : "") + ")");
}
} }
} }

View File

@ -12,294 +12,266 @@
et2_core_common; et2_core_common;
*/ */
import {egw} from "../jsapi/egw_global"; import {egw, IegwAppLocal} from "../jsapi/egw_global";
import {et2_checkType, et2_no_init, et2_validateAttrib} from "./et2_core_common"; import {et2_checkType, et2_cloneObject, et2_no_init, et2_validateAttrib} from "./et2_core_common";
import {et2_implements_registry} from "./et2_core_interfaces"; import {et2_IDOMNode, et2_IInput, et2_IInputNode, et2_implements_registry} from "./et2_core_interfaces";
// Needed for mixin
export function mix (superclass)
{
return new MixinBuilder(superclass);
}
export class MixinBuilder {
constructor(superclass) {
this.superclass = superclass;
}
with(...mixins) {
return mixins.reduce(this.applyMixins, this.superclass);
}
applyMixins(derivedConstructor: any, baseConstructor: any) {
Object.getOwnPropertyNames(baseConstructor.prototype)
.forEach(name => {
Object.defineProperty(derivedConstructor.prototype,
name,
Object.
getOwnPropertyDescriptor(
baseConstructor.prototype,
name
)
);
});
}
copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !== "constructor" && key !== "prototype" && key !== "name") {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
}
// This one from Typescript docs
export function applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
);
});
});
}
/*
Experiments in using mixins to combine et2_widget & LitElement
Note that this "works", in that it mixes the code properly.
It does not work in that the resulting class does not work with et2's inheritance & class checking stuff
// This one to make TypeScript happy?
interface et2_textbox extends et2_textbox, LitElement {}
// This one to make the inheritance magic happen
applyMixins(et2_textbox, [et2_textbox,LitElement]);
// Make it a real WebComponent
customElements.define("et2-textbox",et2_textbox);
*/
export class ClassWithInterfaces export class ClassWithInterfaces
{ {
/** /**
* The implements function can be used to check whether the object * The implements function can be used to check whether the object
* implements the given interface. * implements the given interface.
* *
* As TypeScript can not (yet) check if an objects implements an interface on runtime, * As TypeScript can not (yet) check if an objects implements an interface on runtime,
* we currently implements with each interface a function called 'implements_'+interfacename * we currently implements with each interface a function called 'implements_'+interfacename
* to be able to check here. * to be able to check here.
* *
* @param _iface name of interface to check * @param _iface name of interface to check
*/ */
implements (_iface_name : string) implements(_iface_name: string)
{
if (typeof et2_implements_registry[_iface_name] === 'function' &&
et2_implements_registry[_iface_name](this))
{ {
return true if (typeof et2_implements_registry[_iface_name] === 'function' &&
et2_implements_registry[_iface_name](this))
{
return true
}
return false;
} }
return false;
}
/** /**
* Check if object is an instance of a class or implements an interface (specified by the interfaces name) * Check if object is an instance of a class or implements an interface (specified by the interfaces name)
* *
* @param _class_or_interfacename class(-name) or string with name of interface * @param _class_or_interfacename class(-name) or string with name of interface
*/ */
instanceOf(_class_or_interfacename: any) : boolean instanceOf(_class_or_interfacename: any): boolean
{
if (typeof _class_or_interfacename === 'string')
{ {
return this.implements(_class_or_interfacename); if (typeof _class_or_interfacename === 'string')
{
return this.implements(_class_or_interfacename);
}
return this instanceof _class_or_interfacename;
} }
return this instanceof _class_or_interfacename;
}
} }
export class ClassWithAttributes extends ClassWithInterfaces export class ClassWithAttributes extends ClassWithInterfaces
{ {
/** /**
* Object to collect the attributes we operate on * Object to collect the attributes we operate on
*/ */
attributes: object; attributes: object;
/** /**
* Returns the value of the given attribute. If the property does not * Returns the value of the given attribute. If the property does not
* exist, an error message is issued. * exist, an error message is issued.
* *
* @param {string} _name * @param {string} _name
* @return {*} * @return {*}
*/ */
getAttribute(_name) getAttribute(_name)
{
if (typeof this.attributes[_name] != "undefined" &&
!this.attributes[_name].ignore) {
if (typeof this["get_" + _name] == "function") {
return this["get_" + _name]();
} else {
return this[_name];
}
} else {
egw.debug("error", this, "Attribute '" + _name + "' does not exist!");
}
}
/**
* The setAttribute function sets the attribute with the given name to
* the given value. _override defines, whether this[_name] will be set,
* if this key already exists. _override defaults to true. A warning
* is issued if the attribute does not exist.
*
* @param {string} _name
* @param {*} _value
* @param {boolean} _override
*/
setAttribute(_name, _value, _override)
{
if (typeof this.attributes[_name] != "undefined") {
if (!this.attributes[_name].ignore) {
if (typeof _override == "undefined") {
_override = true;
}
var val = et2_checkType(_value, this.attributes[_name].type,
_name, this);
if (typeof this["set_" + _name] == "function") {
this["set_" + _name](val);
} else if (_override || typeof this[_name] == "undefined") {
this[_name] = val;
}
}
} else {
egw.debug("warn", this, "Attribute '" + _name + "' does not exist!");
}
}
/**
* generateAttributeSet sanitizes the given associative array of attributes
* (by passing each entry to "et2_checkType" and checking for existance of
* the attribute) and adds the default values to the associative array.
*
* @param {object} _attrs is the associative array containing the attributes.
*/
static generateAttributeSet(widget, _attrs)
{
// Sanity check and validation
for (var key in _attrs) {
if (typeof widget[key] != "undefined") {
if (!widget[key].ignore) {
_attrs[key] = et2_checkType(_attrs[key], widget[key].type,
key, this);
}
} else {
// Key does not exist - delete it and issue a warning
delete (_attrs[key]);
egw.debug("warn", this, "Attribute '" + key +
"' does not exist in " + _attrs.type + "!");
}
}
// Include default values or already set values for this attribute
for (var key in widget) {
if (typeof _attrs[key] == "undefined") {
var _default = widget[key]["default"];
if (_default == et2_no_init) {
_default = undefined;
}
_attrs[key] = _default;
}
}
return _attrs;
}
/**
* The initAttributes function sets the attributes to their default
* values. The attributes are not overwritten, which means, that the
* default is only set, if either a setter exists or this[propName] does
* not exist yet.
*
* @param {object} _attrs is the associative array containing the attributes.
*/
initAttributes(_attrs)
{
for (var key in _attrs) {
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) {
this.setAttribute(key, _attrs[key], false);
}
}
}
static buildAttributes(class_prototype: object)
{
let class_tree = [];
let attributes = {};
let n = 0;
do {
n++;
class_tree.push(class_prototype);
class_prototype = Object.getPrototypeOf(class_prototype);
} while (class_prototype !== ClassWithAttributes && n < 50);
for (let i = class_tree.length - 1; i >= 0; i--) {
attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes);
}
return attributes;
}
/**
* Extend current _attributes with the one from the parent class
*
* This gives inheritance from the parent plus the ability to override in the current class.
*
* @param _attributes
* @param _parent
*/
static extendAttributes(_parent: object, _attributes: object): object
{
function _copyMerge(_new, _old)
{ {
var result = {}; if (typeof this.attributes[_name] != "undefined" &&
!this.attributes[_name].ignore)
// Copy the new object {
if (typeof _new != "undefined") { if (typeof this["get_" + _name] == "function")
for (var key in _new) { {
result[key] = _new[key]; return this["get_" + _name]();
}
else
{
return this[_name];
}
} }
} else
{
// Merge the old object egw.debug("error", this, "Attribute '" + _name + "' does not exist!");
for (var key in _old) {
if (typeof result[key] == "undefined") {
result[key] = _old[key];
} }
}
return result;
} }
var attributes = {}; /**
* The setAttribute function sets the attribute with the given name to
* the given value. _override defines, whether this[_name] will be set,
* if this key already exists. _override defaults to true. A warning
* is issued if the attribute does not exist.
*
* @param {string} _name
* @param {*} _value
* @param {boolean} _override
*/
setAttribute(_name, _value, _override)
{
if (typeof this.attributes[_name] != "undefined")
{
if (!this.attributes[_name].ignore)
{
if (typeof _override == "undefined")
{
_override = true;
}
// Copy the old attributes var val = et2_checkType(_value, this.attributes[_name].type,
for (var key in _attributes) { _name, this);
attributes[key] = _copyMerge({}, _attributes[key]);
if (typeof this["set_" + _name] == "function")
{
this["set_" + _name](val);
}
else if (_override || typeof this[_name] == "undefined")
{
this[_name] = val;
}
}
}
else
{
egw.debug("warn", this, "Attribute '" + _name + "' does not exist!");
}
} }
// Add the old attributes to the new ones. If the attributes already /**
// exist, they are merged. * generateAttributeSet sanitizes the given associative array of attributes
for (var key in _parent) { * (by passing each entry to "et2_checkType" and checking for existance of
var _old = _parent[key]; * the attribute) and adds the default values to the associative array.
*
* @param {object} _attrs is the associative array containing the attributes.
*/
static generateAttributeSet(widget, _attrs)
{
// Sanity check and validation
for (var key in _attrs)
{
if (typeof widget[key] != "undefined")
{
if (!widget[key].ignore)
{
_attrs[key] = et2_checkType(_attrs[key], widget[key].type,
key, this);
}
}
else
{
// Key does not exist - delete it and issue a warning
delete (_attrs[key]);
egw.debug("warn", this, "Attribute '" + key +
"' does not exist in " + _attrs.type + "!");
}
}
attributes[key] = _copyMerge(attributes[key], _old); // Include default values or already set values for this attribute
for (var key in widget)
{
if (typeof _attrs[key] == "undefined")
{
var _default = widget[key]["default"];
if (_default == et2_no_init)
{
_default = undefined;
}
_attrs[key] = _default;
}
}
return _attrs;
} }
// Validate the attributes /**
for (var key in attributes) { * The initAttributes function sets the attributes to their default
et2_validateAttrib(key, attributes[key]); * values. The attributes are not overwritten, which means, that the
* default is only set, if either a setter exists or this[propName] does
* not exist yet.
*
* @param {object} _attrs is the associative array containing the attributes.
*/
initAttributes(_attrs)
{
for (var key in _attrs)
{
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined))
{
this.setAttribute(key, _attrs[key], false);
}
}
} }
return attributes; static buildAttributes(class_prototype: object)
} {
let class_tree = [];
let attributes = {};
let n = 0;
do
{
n++;
class_tree.push(class_prototype);
class_prototype = Object.getPrototypeOf(class_prototype);
}
while (class_prototype !== ClassWithAttributes && n < 50);
for (let i = class_tree.length - 1; i >= 0; i--)
{
attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes);
}
return attributes;
}
/**
* Extend current _attributes with the one from the parent class
*
* This gives inheritance from the parent plus the ability to override in the current class.
*
* @param _attributes
* @param _parent
*/
static extendAttributes(_parent: object, _attributes: object): object
{
function _copyMerge(_new, _old)
{
var result = {};
// Copy the new object
if (typeof _new != "undefined")
{
for (var key in _new)
{
result[key] = _new[key];
}
}
// Merge the old object
for (var key in _old)
{
if (typeof result[key] == "undefined")
{
result[key] = _old[key];
}
}
return result;
}
var attributes = {};
// Copy the old attributes
for (var key in _attributes)
{
attributes[key] = _copyMerge({}, _attributes[key]);
}
// Add the old attributes to the new ones. If the attributes already
// exist, they are merged.
for (var key in _parent)
{
var _old = _parent[key];
attributes[key] = _copyMerge(attributes[key], _old);
}
// Validate the attributes
for (var key in attributes)
{
et2_validateAttrib(key, attributes[key]);
}
return attributes;
}
} }

View File

@ -15,17 +15,19 @@
*/ */
import {et2_no_init} from "./et2_core_common"; import {et2_no_init} from "./et2_core_common";
import { ClassWithAttributes } from "./et2_core_inheritance"; import {ClassWithAttributes} from "./et2_core_inheritance";
import { et2_widget, WidgetConfig } from "./et2_core_widget"; import {et2_widget, WidgetConfig} from "./et2_core_widget";
import { et2_valueWidget } from './et2_core_valueWidget' import {et2_valueWidget} from './et2_core_valueWidget'
import {et2_IInput, et2_ISubmitListener} from "./et2_core_interfaces"; import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "./et2_core_interfaces";
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
// fixing circular dependencies by only importing the type (not in compiled .js) // fixing circular dependencies by only importing the type (not in compiled .js)
import type {et2_tabbox} from "./et2_widget_tabs"; import type {et2_tabbox} from "./et2_widget_tabs";
export interface et2_input { export interface et2_input
getInputNode() : HTMLInputElement|HTMLElement; {
getInputNode(): HTMLInputElement | HTMLElement;
} }
/** /**
* et2_inputWidget derrives from et2_simpleWidget and implements the IInput * et2_inputWidget derrives from et2_simpleWidget and implements the IInput
* interface. When derriving from this class, call setDOMNode with an input * interface. When derriving from this class, call setDOMNode with an input
@ -33,9 +35,9 @@ export interface et2_input {
*/ */
export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ISubmitListener, et2_input export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ISubmitListener, et2_input
{ {
static readonly _attributes : any = { static readonly _attributes: any = {
"needed": { "needed": {
"name": "Required", "name": "Required",
"default": false, "default": false,
"type": "boolean", "type": "boolean",
"description": "If required, the user must enter a value before the form can be submitted" "description": "If required, the user must enter a value before the form can be submitted"
@ -78,7 +80,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
/** /**
* Constructor * Constructor
*/ */
constructor(_parent, _attrs? : WidgetConfig, _child? : object) constructor(_parent, _attrs?: WidgetConfig, _child?: object)
{ {
// Call the inherited constructor // Call the inherited constructor
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_inputWidget._attributes, _child || {})); super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_inputWidget._attributes, _child || {}));
@ -105,7 +107,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
/** /**
* Make sure dirty flag is properly set * Make sure dirty flag is properly set
*/ */
doLoadingFinished() : boolean | JQueryPromise<unknown> doLoadingFinished(): boolean | JQueryPromise<unknown>
{ {
let result = super.doLoadingFinished(); let result = super.doLoadingFinished();
@ -141,10 +143,12 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
{ {
jQuery(node) jQuery(node)
.off('.et2_inputWidget') .off('.et2_inputWidget')
.bind("change.et2_inputWidget", this, function(e) { .bind("change.et2_inputWidget", this, function (e)
{
e.data.change.call(e.data, this); e.data.change.call(e.data, this);
}) })
.bind("focus.et2_inputWidget", this, function(e) { .bind("focus.et2_inputWidget", this, function (e)
{
e.data.focus.call(e.data, this); e.data.focus.call(e.data, this);
}); });
} }
@ -173,14 +177,16 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
if (valid && this.onchange) if (valid && this.onchange)
{ {
if(typeof this.onchange == 'function') if (typeof this.onchange == 'function')
{ {
// Make sure function gets a reference to the widget // Make sure function gets a reference to the widget
var args = Array.prototype.slice.call(arguments); var args = Array.prototype.slice.call(arguments);
if(args.indexOf(this) == -1) args.push(this); if (args.indexOf(this) == -1) args.push(this);
return this.onchange.apply(this, args); return this.onchange.apply(this, args);
} else { }
else
{
return (et2_compileLegacyJS(this.options.onchange, this, _node))(); return (et2_compileLegacyJS(this.options.onchange, this, _node))();
} }
} }
@ -189,11 +195,11 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
focus(_node) focus(_node)
{ {
if(typeof this.options.onfocus == 'function') if (typeof this.options.onfocus == 'function')
{ {
// Make sure function gets a reference to the widget // Make sure function gets a reference to the widget
var args = Array.prototype.slice.call(arguments); var args = Array.prototype.slice.call(arguments);
if(args.indexOf(this) == -1) args.push(this); if (args.indexOf(this) == -1) args.push(this);
return this.options.onfocus.apply(this, args); return this.options.onfocus.apply(this, args);
} }
@ -206,13 +212,13 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
* *
* @param {string} _value value to set * @param {string} _value value to set
*/ */
set_value(_value : any | null) set_value(_value: any | null)
{ {
var node = this.getInputNode(); var node = this.getInputNode();
if (node) if (node)
{ {
jQuery(node).val(_value); jQuery(node).val(_value);
if(this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) if (this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value)
{ {
jQuery(node).change(); jQuery(node).change();
} }
@ -223,7 +229,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
set_id(_value) set_id(_value)
{ {
this.id = _value; this.id = _value;
this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId+'_'+this.id : _value; this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId + '_' + this.id : _value;
// Set the id of the _input_ node (in contrast to the default // Set the id of the _input_ node (in contrast to the default
// implementation, which sets the base node) // implementation, which sets the base node)
@ -249,9 +255,12 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
var node = this.getInputNode(); var node = this.getInputNode();
if (node) if (node)
{ {
if(_value && !this.options.readonly) { if (_value && !this.options.readonly)
{
jQuery(node).attr("required", "required"); jQuery(node).attr("required", "required");
} else { }
else
{
node.removeAttribute("required"); node.removeAttribute("required");
} }
} }
@ -273,8 +282,8 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
jQuery(node).addClass("invalid"); jQuery(node).addClass("invalid");
// If on a tab, switch to that tab so user can see it // If on a tab, switch to that tab so user can see it
let widget : et2_widget = this; let widget: et2_widget = this;
while(widget.getParent() && widget.getType() != 'tabbox') while (widget.getParent() && widget.getType() != 'tabbox')
{ {
widget = widget.getParent(); widget = widget.getParent();
} }
@ -319,26 +328,26 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
isDirty() isDirty()
{ {
let value = this.getValue(); let value = this.getValue();
if(typeof value !== typeof this._oldValue) if (typeof value !== typeof this._oldValue)
{ {
return true; return true;
} }
if(this._oldValue === value) if (this._oldValue === value)
{ {
return false; return false;
} }
switch(typeof this._oldValue) switch (typeof this._oldValue)
{ {
case "object": case "object":
if(typeof this._oldValue.length !== "undefined" && if (typeof this._oldValue.length !== "undefined" &&
this._oldValue.length !== value.length this._oldValue.length !== value.length
) )
{ {
return true; return true;
} }
for(let key in this._oldValue) for (let key in this._oldValue)
{ {
if(this._oldValue[key] !== value[key]) return true; if (this._oldValue[key] !== value[key]) return true;
} }
return false; return false;
default: default:
@ -381,5 +390,4 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
this.set_validation_error(valid ? false : messages); this.set_validation_error(valid ? false : messages);
return valid; return valid;
} }
} }

View File

@ -71,7 +71,8 @@ export function et2_compileLegacyJS(_code, _widget, _context)
} }
} }
if (typeof parent[existing_func] === "function") { if (typeof parent[existing_func] === "function") {
return parent[existing_func]; // Bind the object so no matter what happens, context is correct
return parent[existing_func].bind(parent);
} }
} }

View File

@ -22,10 +22,10 @@ import {egw, IegwAppLocal} from "../jsapi/egw_global";
import {et2_cloneObject, et2_csvSplit} from "./et2_core_common"; import {et2_cloneObject, et2_csvSplit} from "./et2_core_common";
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
import {et2_IDOMNode, et2_IInputNode} from "./et2_core_interfaces"; import {et2_IDOMNode, et2_IInputNode} from "./et2_core_interfaces";
import {loadWebComponent} from "./Et2Widget/Et2Widget";
// fixing circular dependencies by only importing type // fixing circular dependencies by only importing type
import type {et2_container} from "./et2_core_baseWidget"; import type {et2_container} from "./et2_core_baseWidget";
import type {et2_inputWidget, et2_input} from "./et2_core_inputWidget"; import type {et2_inputWidget} from "./et2_core_inputWidget";
import {decorateLanguageService} from "ts-lit-plugin/lib/decorate-language-service";
/** /**
* The registry contains all XML tag names and the corresponding widget * The registry contains all XML tag names and the corresponding widget
@ -77,7 +77,7 @@ export function et2_register_widget(_constructor, _types)
* is not passed, it will default to null. Then you have to attach the element * is not passed, it will default to null. Then you have to attach the element
* to a parent using the addChild or insertChild method. * to a parent using the addChild or insertChild method.
*/ */
export function et2_createWidget(_name : string, _attrs : object, _parent? : any) : et2_widget export function et2_createWidget(_name: string, _attrs: object, _parent?: any): et2_widget
{ {
"use strict"; "use strict";
@ -119,10 +119,12 @@ export function et2_createWidget(_name : string, _attrs : object, _parent? : any
// make et2_createWidget publicly available as we need to call it from stylite/js/gantt.js (maybe others) // make et2_createWidget publicly available as we need to call it from stylite/js/gantt.js (maybe others)
if (typeof window.et2_createWidget === 'undefined') window['et2_createWidget'] = et2_createWidget; if (typeof window.et2_createWidget === 'undefined') window['et2_createWidget'] = et2_createWidget;
export interface WidgetConfig { export interface WidgetConfig
{
type?: string; type?: string;
readonly?: boolean; readonly?: boolean;
width?: number; width?: number;
[propName: string]: any; [propName: string]: any;
} }
@ -191,7 +193,7 @@ export class et2_widget extends ClassWithAttributes
private _type: string; private _type: string;
id: string; id: string;
supportedWidgetClasses : any[]; supportedWidgetClasses: any[];
options: WidgetConfig; options: WidgetConfig;
readonly: boolean; readonly: boolean;
@ -208,21 +210,24 @@ export class et2_widget extends ClassWithAttributes
* @param _attrs is an associative array of attributes. * @param _attrs is an associative array of attributes.
* @param _child attributes object from the child * @param _child attributes object from the child
*/ */
constructor(_parent?, _attrs? : WidgetConfig, _child? : object) constructor(_parent?, _attrs?: WidgetConfig, _child?: object)
{ {
super(); // because we in the top of the widget hierarchy super(); // because we in the top of the widget hierarchy
this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {}); this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {});
// Check whether all attributes are available // Check whether all attributes are available
if (typeof _parent == "undefined") { if (typeof _parent == "undefined")
{
_parent = null; _parent = null;
} }
if (typeof _attrs == "undefined") { if (typeof _attrs == "undefined")
{
_attrs = {}; _attrs = {};
} }
if (_attrs.attributes) { if (_attrs.attributes)
{
jQuery.extend(_attrs, _attrs.attributes); jQuery.extend(_attrs, _attrs.attributes);
} }
// Initialize all important parameters // Initialize all important parameters
@ -233,22 +238,26 @@ export class et2_widget extends ClassWithAttributes
this.id = _attrs["id"]; this.id = _attrs["id"];
// Add this widget to the given parent widget // Add this widget to the given parent widget
if (_parent != null) { if (_parent != null)
{
_parent.addChild(this); _parent.addChild(this);
} }
// The supported widget classes array defines a whitelist for all widget // The supported widget classes array defines a whitelist for all widget
// classes or interfaces child widgets have to support. // classes or interfaces child widgets have to support.
this.supportedWidgetClasses = [et2_widget]; this.supportedWidgetClasses = [et2_widget, HTMLElement];
if (_attrs["id"]) { if (_attrs["id"])
{
// Create a namespace for this object // Create a namespace for this object
if (this._createNamespace()) { if (this._createNamespace())
{
this.checkCreateNamespace(_attrs); this.checkCreateNamespace(_attrs);
} }
} }
if (this.id) { if (this.id)
{
//this.id = this.id.replace(/\[/g,'&#x5B;').replace(/]/g,'&#x5D;'); //this.id = this.id.replace(/\[/g,'&#x5B;').replace(/]/g,'&#x5D;');
} }
@ -271,29 +280,33 @@ export class et2_widget extends ClassWithAttributes
destroy() destroy()
{ {
// Call the destructor of all children // Call the destructor of all children
for (var i = this._children.length - 1; i >= 0; i--) { for (var i = this._children.length - 1; i >= 0; i--)
{
this._children[i].destroy(); this._children[i].destroy();
} }
// Remove this element from the parent, if it exists // Remove this element from the parent, if it exists
if (typeof this._parent != "undefined" && this._parent !== null) { if (typeof this._parent != "undefined" && this._parent !== null)
{
this._parent.removeChild(this); this._parent.removeChild(this);
} }
// Free the array managers if they belong to this widget // Free the array managers if they belong to this widget
for (var key in this._mgrs) { for (var key in this._mgrs)
if (this._mgrs[key] && this._mgrs[key].owner == this) { {
if (this._mgrs[key] && this._mgrs[key].owner == this)
{
this._mgrs[key].destroy(); this._mgrs[key].destroy();
} }
} }
} }
getType() : string getType(): string
{ {
return this._type; return this._type;
} }
setType(_type : string) setType(_type: string)
{ {
this._type = _type; this._type = _type;
} }
@ -308,7 +321,8 @@ export class et2_widget extends ClassWithAttributes
clone(_parent) clone(_parent)
{ {
// Default _parent to null // Default _parent to null
if (typeof _parent == "undefined") { if (typeof _parent == "undefined")
{
_parent = null; _parent = null;
} }
@ -323,12 +337,14 @@ export class et2_widget extends ClassWithAttributes
assign(_obj) assign(_obj)
{ {
if (typeof _obj._children == "undefined") { if (typeof _obj._children == "undefined")
{
this.egw().debug("log", "Foo!"); this.egw().debug("log", "Foo!");
} }
// Create a clone of all child elements of the given object // Create a clone of all child elements of the given object
for (var i = 0; i < _obj._children.length; i++) { for (var i = 0; i < _obj._children.length; i++)
{
_obj._children[i].clone(this); _obj._children[i].clone(this);
} }
@ -341,7 +357,7 @@ export class et2_widget extends ClassWithAttributes
/** /**
* Returns the parent widget of this widget * Returns the parent widget of this widget
*/ */
getParent() : et2_widget | null getParent(): et2_widget | null
{ {
return this._parent; return this._parent;
} }
@ -351,7 +367,7 @@ export class et2_widget extends ClassWithAttributes
/** /**
* Returns the list of children of this widget. * Returns the list of children of this widget.
*/ */
getChildren() : et2_widget[] getChildren(): et2_widget[]
{ {
return this._children; return this._children;
} }
@ -359,11 +375,14 @@ export class et2_widget extends ClassWithAttributes
/** /**
* Returns the base widget * Returns the base widget
*/ */
getRoot() : et2_container getRoot(): et2_container
{ {
if (this._parent != null) { if (this._parent != null)
{
return this._parent.getRoot(); return this._parent.getRoot();
} else { }
else
{
return <et2_container><unknown>this; return <et2_container><unknown>this;
} }
} }
@ -374,7 +393,7 @@ export class et2_widget extends ClassWithAttributes
* @param _node is the node which should be added. It has to be an instance * @param _node is the node which should be added. It has to be an instance
* of et2_widget * of et2_widget
*/ */
addChild(_node : et2_widget) addChild(_node: et2_widget)
{ {
this.insertChild(_node, this._children.length); this.insertChild(_node, this._children.length);
} }
@ -386,18 +405,22 @@ export class et2_widget extends ClassWithAttributes
* of et2_widget * of et2_widget
* @param _idx is the position at which the element should be added. * @param _idx is the position at which the element should be added.
*/ */
insertChild(_node : et2_widget, _idx: number) insertChild(_node: et2_widget, _idx: number)
{ {
// Check whether the node is one of the supported widget classes. // Check whether the node is one of the supported widget classes.
if (this.isOfSupportedWidgetClass(_node)) { if (this.isOfSupportedWidgetClass(_node))
{
// Remove the node from its original parent // Remove the node from its original parent
if (_node._parent) { if (_node._parent)
{
_node._parent.removeChild(_node); _node._parent.removeChild(_node);
} }
_node._parent = this; _node._parent = this;
this._children.splice(_idx, 0, _node); this._children.splice(_idx, 0, _node);
} else { }
else
{
this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this); this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this);
// throw("Widget is not supported by this widget class!"); // throw("Widget is not supported by this widget class!");
} }
@ -413,7 +436,8 @@ export class et2_widget extends ClassWithAttributes
// Retrieve the child from the child list // Retrieve the child from the child list
var idx = this._children.indexOf(_node); var idx = this._children.indexOf(_node);
if (idx >= 0) { if (idx >= 0)
{
// This element is no longer parent of the child // This element is no longer parent of the child
_node._parent = null; _node._parent = null;
@ -426,24 +450,29 @@ export class et2_widget extends ClassWithAttributes
* *
* @param _id is the id you're searching for * @param _id is the id you're searching for
*/ */
getWidgetById(_id) : et2_widget | null getWidgetById(_id): et2_widget | null
{ {
if (this.id == _id) { if (this.id == _id)
{
return this; return this;
} }
if (!this._children) return null; if (!this._children) return null;
for (var i = 0; i < this._children.length; i++) { for (var i = 0; i < this._children.length; i++)
{
var elem = this._children[i].getWidgetById(_id); var elem = this._children[i].getWidgetById(_id);
if (elem != null) { if (elem != null)
{
return elem; return elem;
} }
} }
if (this.id && _id.indexOf('[') > -1 && this._children.length) { if (this.id && _id.indexOf('[') > -1 && this._children.length)
{
var ids = (new et2_arrayMgr()).explodeKey(_id); var ids = (new et2_arrayMgr()).explodeKey(_id);
var widget : et2_widget = this; var widget: et2_widget = this;
for (var i = 0; i < ids.length && widget !== null; i++) { for (var i = 0; i < ids.length && widget !== null; i++)
{
widget = widget.getWidgetById(ids[i]); widget = widget.getWidgetById(ids[i]);
} }
return widget; return widget;
@ -462,15 +491,18 @@ export class et2_widget extends ClassWithAttributes
*/ */
iterateOver(_callback, _context, _type?) iterateOver(_callback, _context, _type?)
{ {
if (typeof _type == "undefined") { if (typeof _type == "undefined")
{
_type = et2_widget; _type = et2_widget;
} }
if (this.isInTree() && this.instanceOf(_type)) { if (this.isInTree() && this.instanceOf(_type))
{
_callback.call(_context, this); _callback.call(_context, this);
} }
for (var i = 0; i < this._children.length; i++) { for (var i = 0; i < this._children.length; i++)
{
this._children[i].iterateOver(_callback, _context, _type); this._children[i].iterateOver(_callback, _context, _type);
} }
} }
@ -486,13 +518,15 @@ export class et2_widget extends ClassWithAttributes
* return this._super(inTree); * return this._super(inTree);
* when calling this function the _vis parameter does not have to be supplied. * when calling this function the _vis parameter does not have to be supplied.
*/ */
isInTree(_sender?, _vis? : boolean) isInTree(_sender?, _vis?: boolean)
{ {
if (typeof _vis == "undefined") { if (typeof _vis == "undefined")
{
_vis = true; _vis = true;
} }
if (this._parent) { if (this._parent)
{
return _vis && this._parent.isInTree(this); return _vis && this._parent.isInTree(this);
} }
@ -501,8 +535,10 @@ export class et2_widget extends ClassWithAttributes
isOfSupportedWidgetClass(_obj) isOfSupportedWidgetClass(_obj)
{ {
for (var i = 0; i < this.supportedWidgetClasses.length; i++) { for (var i = 0; i < this.supportedWidgetClasses.length; i++)
if (_obj instanceof this.supportedWidgetClasses[i]) { {
if (_obj instanceof this.supportedWidgetClasses[i])
{
return true; return true;
} }
} }
@ -521,47 +557,55 @@ export class et2_widget extends ClassWithAttributes
parseXMLAttrs(_attrsObj, _target, _proto) parseXMLAttrs(_attrsObj, _target, _proto)
{ {
// Check whether the attributes object is really existing, if not abort // Check whether the attributes object is really existing, if not abort
if (typeof _attrsObj == "undefined") { if (typeof _attrsObj == "undefined")
{
return; return;
} }
// Iterate over the given attributes and parse them // Iterate over the given attributes and parse them
var mgr = this.getArrayMgr("content"); var mgr = this.getArrayMgr("content");
for (var i = 0; i < _attrsObj.length; i++) { for (var i = 0; i < _attrsObj.length; i++)
{
var attrName = _attrsObj[i].name; var attrName = _attrsObj[i].name;
var attrValue = _attrsObj[i].value; var attrValue = _attrsObj[i].value;
// Special handling for the legacy options // Special handling for the legacy options
if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0) { if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0)
{
let legacy = _proto.constructor.legacyOptions || []; let legacy = _proto.constructor.legacyOptions || [];
let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {}; let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {};
// Check for modifications on legacy options here. Normal modifications // Check for modifications on legacy options here. Normal modifications
// are handled in widget constructor, but it's too late for legacy options then // are handled in widget constructor, but it's too late for legacy options then
if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) { if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id))
var mod : any = this.getArrayMgr("modifications").getEntry(_target.id); {
var mod: any = this.getArrayMgr("modifications").getEntry(_target.id);
if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options; if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options;
} }
// expand legacyOptions with content // expand legacyOptions with content
if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1)
{
attrValue = mgr.expandName(attrValue); attrValue = mgr.expandName(attrValue);
} }
// Parse the legacy options (as a string, other types not allowed) // Parse the legacy options (as a string, other types not allowed)
var splitted = et2_csvSplit(attrValue + ""); var splitted = et2_csvSplit(attrValue + "");
for (var j = 0; j < splitted.length && j < legacy.length; j++) { for (var j = 0; j < splitted.length && j < legacy.length; j++)
{
// Blank = not set, unless there's more legacy options provided after // Blank = not set, unless there's more legacy options provided after
if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue; if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue;
// Check to make sure we don't overwrite a current option with a legacy option // Check to make sure we don't overwrite a current option with a legacy option
if (typeof _target[legacy[j]] === "undefined") { if (typeof _target[legacy[j]] === "undefined")
{
attrValue = splitted[j]; attrValue = splitted[j];
/** /**
If more legacy options than expected, stuff them all in the last legacy option If more legacy options than expected, stuff them all in the last legacy option
Some legacy options take a comma separated list. Some legacy options take a comma separated list.
*/ */
if (j == legacy.length - 1 && splitted.length > legacy.length) { if (j == legacy.length - 1 && splitted.length > legacy.length)
{
attrValue = splitted.slice(j); attrValue = splitted.slice(j);
} }
@ -569,26 +613,37 @@ export class et2_widget extends ClassWithAttributes
// If the attribute is marked as boolean, parse the // If the attribute is marked as boolean, parse the
// expression as bool expression. // expression as bool expression.
if (attr.type == "boolean") { if (attr.type == "boolean")
{
attrValue = mgr.parseBoolExpression(attrValue); attrValue = mgr.parseBoolExpression(attrValue);
} else if (typeof attrValue != "object") { }
else if (typeof attrValue != "object")
{
attrValue = mgr.expandName(attrValue); attrValue = mgr.expandName(attrValue);
} }
_target[legacy[j]] = attrValue; _target[legacy[j]] = attrValue;
} }
} }
} else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { }
else if (attrName == "readonly" && typeof _target[attrName] != "undefined")
{
// do NOT overwrite already evaluated readonly attribute // do NOT overwrite already evaluated readonly attribute
} else { }
else
{
let attrs = et2_attribute_registry[_proto.constructor.name] || {}; let attrs = et2_attribute_registry[_proto.constructor.name] || {};
if (mgr != null && typeof attrs[attrName] != "undefined") { if (mgr != null && typeof attrs[attrName] != "undefined")
{
var attr = attrs[attrName]; var attr = attrs[attrName];
// If the attribute is marked as boolean, parse the // If the attribute is marked as boolean, parse the
// expression as bool expression. // expression as bool expression.
if (attr.type == "boolean") { if (attr.type == "boolean")
{
attrValue = mgr.parseBoolExpression(attrValue); attrValue = mgr.parseBoolExpression(attrValue);
} else { }
else
{
attrValue = mgr.expandName(attrValue); attrValue = mgr.expandName(attrValue);
} }
} }
@ -609,20 +664,26 @@ export class et2_widget extends ClassWithAttributes
{ {
// Apply the content of the modifications array // Apply the content of the modifications array
if (this.id) { if (this.id)
if (typeof this.id != "string") { {
if (typeof this.id != "string")
{
console.log(this.id); console.log(this.id);
} }
if (this.getArrayMgr("modifications")) { if (this.getArrayMgr("modifications"))
{
var data = this.getArrayMgr("modifications").getEntry(this.id); var data = this.getArrayMgr("modifications").getEntry(this.id);
// Check for already inside namespace // Check for already inside namespace
if (this._createNamespace() && this.getArrayMgr("modifications").perspectiveData.owner == this) { if (this._createNamespace() && this.getArrayMgr("modifications").perspectiveData.owner == this)
{
data = this.getArrayMgr("modifications").data; data = this.getArrayMgr("modifications").data;
} }
if (typeof data === 'object') { if (typeof data === 'object')
for (var key in data) { {
for (var key in data)
{
_attrs[key] = data[key]; _attrs[key] = data[key];
} }
} }
@ -630,16 +691,19 @@ export class et2_widget extends ClassWithAttributes
} }
// Translate the attributes // Translate the attributes
for (var key in _attrs) { for (var key in _attrs)
if (_attrs[key] && typeof this.attributes[key] != "undefined") { {
if (_attrs[key] && typeof this.attributes[key] != "undefined")
{
if (this.attributes[key].translate === true || if (this.attributes[key].translate === true ||
(this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"])) { (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"]))
{
let value = _attrs[key]; let value = _attrs[key];
// allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname}
if (value.indexOf('{') !== -1) if (value.indexOf('{') !== -1)
{ {
const egw = this.egw(); const egw = this.egw();
_attrs[key] = value.replace(/{([^}]+)}/g, function(str,p1) _attrs[key] = value.replace(/{([^}]+)}/g, function (str, p1)
{ {
return egw.lang(p1); return egw.lang(p1);
}); });
@ -674,28 +738,34 @@ export class et2_widget extends ClassWithAttributes
// Parse the "readonly" and "type" flag for this element here, as they // Parse the "readonly" and "type" flag for this element here, as they
// determine which constructor is used // determine which constructor is used
var _nodeName = attributes["type"] = _node.getAttribute("type") ? var _nodeName = attributes["type"] = _node.getAttribute("type") ?
_node.getAttribute("type") : _node.nodeName.toLowerCase(); _node.getAttribute("type") : _node.nodeName.toLowerCase();
var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ? var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ?
(<any>this.getArrayMgr("readonlys")).isReadOnly( (<any>this.getArrayMgr("readonlys")).isReadOnly(
_node.getAttribute("id"), _node.getAttribute("readonly"), _node.getAttribute("id"), _node.getAttribute("readonly"),
typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false; typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false;
// Check to see if modifications change type // Check to see if modifications change type
var modifications = this.getArrayMgr("modifications"); var modifications = this.getArrayMgr("modifications");
if (modifications && _node.getAttribute("id")) { if (modifications && _node.getAttribute("id"))
var entry : any = modifications.getEntry(_node.getAttribute("id")); {
if (entry == null) { var entry: any = modifications.getEntry(_node.getAttribute("id"));
if (entry == null)
{
// Try again, but skip the fancy stuff // Try again, but skip the fancy stuff
// TODO: Figure out why the getEntry() call doesn't always work // TODO: Figure out why the getEntry() call doesn't always work
var entry = modifications.data[_node.getAttribute("id")]; var entry = modifications.data[_node.getAttribute("id")];
if (entry) { if (entry)
{
this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry); this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry);
} else { }
else
{
// Try the root, in case a namespace got missed // Try the root, in case a namespace got missed
entry = modifications.getRoot().getEntry(_node.getAttribute("id")); entry = modifications.getRoot().getEntry(_node.getAttribute("id"));
} }
} }
if (entry && entry.type && typeof entry.type === 'string') { if (entry && entry.type && typeof entry.type === 'string')
{
_nodeName = attributes["type"] = entry.type; _nodeName = attributes["type"] = entry.type;
} }
entry = null; entry = null;
@ -703,25 +773,27 @@ export class et2_widget extends ClassWithAttributes
// if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"), // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"),
// we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs! // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs!
if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0) { if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0)
{
_nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName); _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName);
} }
// Get the constructor - if the widget is readonly, use the special "_ro" // Get the constructor - if the widget is readonly, use the special "_ro"
// constructor if it is available // constructor if it is available
var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName]; var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName];
if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined") { if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined")
{
constructor = et2_registry[_nodeName + "_ro"]; constructor = et2_registry[_nodeName + "_ro"];
} }
// Parse the attributes from the given XML attributes object if (undefined == window.customElements.get(_nodeName))
this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
// Do an sanity check for the attributes
ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
if(undefined == window.customElements.get(_nodeName))
{ {
// Parse the attributes from the given XML attributes object
this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
// Do an sanity check for the attributes
ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
// Creates the new widget, passes this widget as an instance and // Creates the new widget, passes this widget as an instance and
// passes the widgetType. Then it goes on loading the XML for it. // passes the widgetType. Then it goes on loading the XML for it.
var widget = new constructor(this, attributes); var widget = new constructor(this, attributes);
@ -731,33 +803,11 @@ export class et2_widget extends ClassWithAttributes
} }
else else
{ {
widget = this.loadWebComponent(_nodeName, _node, attributes); widget = loadWebComponent(_nodeName, _node, this);
if(this.addChild)
{
// webcomponent going into old et2_widget
this.addChild(widget);
}
} }
return widget; return widget;
} }
/**
* Load a Web Component
* @param _nodeName
* @param _node
*/
loadWebComponent(_nodeName : string, _node, attributes : Object) : HTMLElement
{
let widget = document.createElement(_nodeName);
widget.textContent = _node.textContent;
// Apply any set attributes
_node.getAttributeNames().forEach(attribute => widget.setAttribute(attribute, attributes[attribute]));
return widget;
}
/** /**
* Loads the widget tree from an XML node * Loads the widget tree from an XML node
* *
@ -766,16 +816,20 @@ export class et2_widget extends ClassWithAttributes
loadFromXML(_node) loadFromXML(_node)
{ {
// Load the child nodes. // Load the child nodes.
for (var i = 0; i < _node.childNodes.length; i++) { for (var i = 0; i < _node.childNodes.length; i++)
{
var node = _node.childNodes[i]; var node = _node.childNodes[i];
var widgetType = node.nodeName.toLowerCase(); var widgetType = node.nodeName.toLowerCase();
if (widgetType == "#comment") { if (widgetType == "#comment")
{
continue; continue;
} }
if (widgetType == "#text") { if (widgetType == "#text")
if (node.data.replace(/^\s+|\s+$/g, '')) { {
if (node.data.replace(/^\s+|\s+$/g, ''))
{
this.loadContent(node.data); this.loadContent(node.data);
} }
continue; continue;
@ -823,29 +877,39 @@ export class et2_widget extends ClassWithAttributes
// Make sure promises is defined to avoid errors. // Make sure promises is defined to avoid errors.
// We'll warn (below) if programmer should have passed it. // We'll warn (below) if programmer should have passed it.
if (typeof promises == "undefined") { if (typeof promises == "undefined")
{
promises = []; promises = [];
var warn_if_deferred = true; var warn_if_deferred = true;
} }
var loadChildren = function () { var loadChildren = function ()
{
// Descend recursively into the tree // Descend recursively into the tree
for (var i = 0; i < this._children.length; i++) { for (var i = 0; i < this._children.length; i++)
try { {
try
{
this._children[i].loadingFinished(promises); this._children[i].loadingFinished(promises);
} catch (e) { }
catch (e)
{
egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack); egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack);
} }
} }
}; };
var result = this.doLoadingFinished(); var result = this.doLoadingFinished();
if (typeof result == "boolean" && result) { if (typeof result == "boolean" && result)
{
// Simple widget finishes nicely // Simple widget finishes nicely
loadChildren.apply(this, arguments); loadChildren.apply(this, arguments);
} else if (typeof result == "object" && result.done) { }
else if (typeof result == "object" && result.done)
{
// Warn if list was not provided // Warn if list was not provided
if (warn_if_deferred) { if (warn_if_deferred)
{
// Might not be a problem, but if you need the widget to be really loaded, it could be // Might not be a problem, but if you need the widget to be really loaded, it could be
egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this);
} }
@ -868,14 +932,17 @@ export class et2_widget extends ClassWithAttributes
*/ */
initAttributes(_attrs) initAttributes(_attrs)
{ {
for (var key in _attrs) { for (var key in _attrs)
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { {
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined))
{
var val = _attrs[key]; var val = _attrs[key];
// compile string values of attribute type "js" to functions // compile string values of attribute type "js" to functions
if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string') { if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string')
{
val = et2_compileLegacyJS(val, this, val = et2_compileLegacyJS(val, this,
this.implements(et2_IInputNode) ? (<et2_inputWidget><unknown>this).getInputNode() : this.implements(et2_IInputNode) ? (<et2_inputWidget><unknown>this).getInputNode() :
(this.implements(et2_IDOMNode) ? (<et2_IDOMNode><unknown>this).getDOMNode() : null)); (this.implements(et2_IDOMNode) ? (<et2_IDOMNode><unknown>this).getDOMNode() : null));
} }
this.setAttribute(key, val, false); this.setAttribute(key, val, false);
} }
@ -891,7 +958,7 @@ export class et2_widget extends ClassWithAttributes
* *
* @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise} * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise}
*/ */
doLoadingFinished() : JQueryPromise<any> | boolean doLoadingFinished(): JQueryPromise<any> | boolean
{ {
return true; return true;
} }
@ -903,19 +970,23 @@ export class et2_widget extends ClassWithAttributes
* to this widget tree. The api instance can be set in the "container" * to this widget tree. The api instance can be set in the "container"
* widget using the setApiInstance function. * widget using the setApiInstance function.
*/ */
egw() : IegwAppLocal egw(): IegwAppLocal
{ {
// The _egw property is not set // The _egw property is not set
if (typeof this._egw === 'undefined') { if (typeof this._egw === 'undefined')
if (this._parent != null) { {
if (this._parent != null)
{
return this._parent.egw(); return this._parent.egw();
} }
// Get the window this object belongs to // Get the window this object belongs to
var wnd = null; var wnd = null;
if (this.implements(et2_IDOMNode)) { if (this.implements(et2_IDOMNode))
{
var node = (<et2_IDOMNode><unknown>this).getDOMNode(); var node = (<et2_IDOMNode><unknown>this).getDOMNode();
if (node && node.ownerDocument) { if (node && node.ownerDocument)
{
wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView;
} }
} }
@ -933,7 +1004,7 @@ export class et2_widget extends ClassWithAttributes
* *
* @param {IegwAppLocal} _egw egw object to set * @param {IegwAppLocal} _egw egw object to set
*/ */
setApiInstance(_egw : IegwAppLocal) setApiInstance(_egw: IegwAppLocal)
{ {
this._egw = _egw; this._egw = _egw;
} }
@ -956,22 +1027,26 @@ export class et2_widget extends ClassWithAttributes
* *
* @param _mgrs is used internally and should not be supplied. * @param _mgrs is used internally and should not be supplied.
*/ */
getArrayMgrs(_mgrs? : object) getArrayMgrs(_mgrs?: object)
{ {
if (typeof _mgrs == "undefined") { if (typeof _mgrs == "undefined")
{
_mgrs = {}; _mgrs = {};
} }
// Add all managers of this object to the result, if they have not already // Add all managers of this object to the result, if they have not already
// been set in the result // been set in the result
for (var key in this._mgrs) { for (var key in this._mgrs)
if (typeof _mgrs[key] == "undefined") { {
if (typeof _mgrs[key] == "undefined")
{
_mgrs[key] = this._mgrs[key]; _mgrs[key] = this._mgrs[key];
} }
} }
// Recursively applies this function to the parent widget // Recursively applies this function to the parent widget
if (this._parent) { if (this._parent)
{
this._parent.getArrayMgrs(_mgrs); this._parent.getArrayMgrs(_mgrs);
} }
@ -984,7 +1059,7 @@ export class et2_widget extends ClassWithAttributes
* @param {string} _part which array mgr to set * @param {string} _part which array mgr to set
* @param {object} _mgr * @param {object} _mgr
*/ */
setArrayMgr(_part : string, _mgr : et2_arrayMgr) setArrayMgr(_part: string, _mgr: et2_arrayMgr)
{ {
this._mgrs[_part] = _mgr; this._mgrs[_part] = _mgr;
} }
@ -994,11 +1069,14 @@ export class et2_widget extends ClassWithAttributes
* *
* @param {string} managed_array_type name of array mgr to return * @param {string} managed_array_type name of array mgr to return
*/ */
getArrayMgr(managed_array_type : string) : et2_arrayMgr | null getArrayMgr(managed_array_type: string): et2_arrayMgr | null
{ {
if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined") { if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined")
{
return this._mgrs[managed_array_type]; return this._mgrs[managed_array_type];
} else if (this._parent) { }
else if (this._parent)
{
return this._parent.getArrayMgr(managed_array_type); return this._parent.getArrayMgr(managed_array_type);
} }
@ -1012,27 +1090,32 @@ export class et2_widget extends ClassWithAttributes
* *
* Constructor attributes are passed in case a child needs to make decisions * Constructor attributes are passed in case a child needs to make decisions
*/ */
checkCreateNamespace(_attrs? : any) checkCreateNamespace(_attrs?: any)
{ {
// Get the content manager // Get the content manager
var mgrs = this.getArrayMgrs(); var mgrs = this.getArrayMgrs();
for (var key in mgrs) { for (var key in mgrs)
{
var mgr = mgrs[key]; var mgr = mgrs[key];
// Get the original content manager if we have already created a // Get the original content manager if we have already created a
// perspective for this node // perspective for this node
if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) { if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this)
{
mgr = mgr.parentMgr; mgr = mgr.parentMgr;
} }
// Check whether the manager has a namespace for the id of this object // Check whether the manager has a namespace for the id of this object
var entry = mgr.getEntry(this.id); var entry = mgr.getEntry(this.id);
if (typeof entry === 'object' && entry !== null || this.id) { if (typeof entry === 'object' && entry !== null || this.id)
{
// The content manager has an own node for this object, so // The content manager has an own node for this object, so
// create an own perspective. // create an own perspective.
this._mgrs[key] = mgr.openPerspective(this, this.id); this._mgrs[key] = mgr.openPerspective(this, this.id);
} else { }
else
{
// The current content manager does not have an own namespace for // The current content manager does not have an own namespace for
// this element, so use the content manager of the parent. // this element, so use the content manager of the parent.
delete (this._mgrs[key]); delete (this._mgrs[key]);
@ -1048,7 +1131,7 @@ export class et2_widget extends ClassWithAttributes
* *
* @private * @private
*/ */
protected _createNamespace() : boolean protected _createNamespace(): boolean
{ {
return false; return false;
} }
@ -1077,9 +1160,12 @@ export class et2_widget extends ClassWithAttributes
*/ */
getInstanceManager() getInstanceManager()
{ {
if (this._inst != null) { if (this._inst != null)
{
return this._inst; return this._inst;
} else if (this._parent) { }
else if (this._parent)
{
return this._parent.getInstanceManager(); return this._parent.getInstanceManager();
} }

View File

@ -1759,17 +1759,9 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
{ {
continue; continue;
} }
if(typeof _row[x] != "undefined" && _row[x].widget)
{
columnWidgets[x] = _row[x].widget;
// Append the widget to this container columnWidgets[x] = _row[x].widget;
this.addChild(_row[x].widget);
}
else
{
columnWidgets[x] = _row[x].widget;
}
// Pass along column alignment // Pass along column alignment
if(_row[x].align && columnWidgets[x]) if(_row[x].align && columnWidgets[x])
{ {
@ -3848,14 +3840,27 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext
return this.filter_div[0]; return this.filter_div[0];
} }
} }
if(_sender == this.et2_searchbox) return this.search_box[0]; if(_sender == this.et2_searchbox)
if(_sender.id == 'export') return this.right_div[0]; {
return this.search_box[0];
}
if(_sender == this.favorites)
{
return egwIsMobile() ? this.search_box.find('.nm_favorites_div').show()[0] : this.right_div[0];
}
if(_sender.id == 'export')
{
return this.right_div[0];
}
if(_sender && _sender._type == "template") if(_sender && _sender._type == "template")
{ {
for(let i = 0; i < this.headers.length; i++) for(let i = 0; i < this.headers.length; i++)
{ {
if(_sender.id == this.headers[i].id && _sender._parent == this) return i == 2 ? this.header_row[0] : this.header_div[0]; if(_sender.id == this.headers[i].id && _sender._parent == this)
{
return i == 2 ? this.header_row[0] : this.header_div[0];
}
} }
} }
return null; return null;

View File

@ -193,7 +193,7 @@ export class et2_nextmatch_rowProvider
} }
// Adjust data for that row // Adjust data for that row
entry.widget.transformAttributes.call(entry.widget,data); entry.widget.transformAttributes?.call(entry.widget, data);
// Call the setDetachedAttributes function // Call the setDetachedAttributes function
entry.widget.setDetachedAttributes(nodes, data, _data); entry.widget.setDetachedAttributes(nodes, data, _data);
@ -282,20 +282,33 @@ export class et2_nextmatch_rowProvider
// Get all attribute values // Get all attribute values
for (const key in _widget.attributes) for (const key in _widget.attributes)
{ {
if(typeof _widget.attributes[key] !== "object")
{
continue;
}
let attr_name = key;
let val;
if(!_widget.attributes[key].ignore && if(!_widget.attributes[key].ignore &&
typeof _widget.options != "undefined" &&
typeof _widget.options[key] != "undefined") typeof _widget.options[key] != "undefined")
{ {
const val = _widget.options[key]; val = _widget.options[key];
}
// TODO: Improve detection // Handle web components
if(typeof val == "string" && val.indexOf("$") >= 0) else if(_widget.attributes[key].value)
{ {
hasAttr = true; val = _widget.attributes[key].value;
widgetData.data.push({ attr_name = _widget.attributes[key].name;
"attribute": key, }
"expression": val // TODO: Improve detection
}); if(typeof val == "string" && val.indexOf("$") >= 0)
} {
hasAttr = true;
widgetData.data.push({
"attribute": attr_name,
"expression": val
});
} }
} }

View File

@ -66,7 +66,7 @@ export class et2_box extends et2_baseWidget implements et2_IDetachedDOM
* @param {object} _node * @param {object} _node
*/ */
loadFromXML(_node) { loadFromXML(_node) {
if(this.getType() != "box") if(this.getType() != "old-box")
{ {
return super.loadFromXML(_node); return super.loadFromXML(_node);
} }
@ -162,7 +162,7 @@ export class et2_box extends et2_baseWidget implements et2_IDetachedDOM
} }
} }
et2_register_widget(et2_box, ["vbox", "box"]); et2_register_widget(et2_box, ["old-vbox", "old-box"]);
/** /**
* Details widget implementation * Details widget implementation

View File

@ -20,7 +20,7 @@ import {et2_register_widget, WidgetConfig} from "./et2_core_widget";
import {et2_baseWidget} from './et2_core_baseWidget' import {et2_baseWidget} from './et2_core_baseWidget'
import {et2_inputWidget} from "./et2_core_inputWidget"; import {et2_inputWidget} from "./et2_core_inputWidget";
import {expose} from "./expose"; import {expose} from "./expose";
import {et2_IDetachedDOM, et2_IExposable} from "./et2_core_interfaces"; import {et2_IDetachedDOM, et2_IExposable, et2_IInputNode} from "./et2_core_interfaces";
import {egw} from "../jsapi/egw_global"; import {egw} from "../jsapi/egw_global";
/** /**
@ -168,10 +168,10 @@ export class et2_description extends expose(class et2_description extends et2_ba
(for_widget = this.getRoot().getWidgetById(this.options.for)) (for_widget = this.getRoot().getWidgetById(this.options.for))
) && for_widget && for_widget.id) ) && for_widget && for_widget.id)
{ {
if(for_widget.dom_id) if(for_widget.dom_id || for_widget.getDOMNode().id)
{ {
for_id = for_widget.dom_id; for_id = for_widget.dom_id || for_widget.getDOMNode().id;
if(for_widget.instanceOf(et2_inputWidget) && for_widget.getInputNode() && for_widget.dom_id !== for_widget.getInputNode().id) if(for_widget.instanceOf(et2_IInputNode) && for_widget.getInputNode() && for_id !== for_widget.getInputNode().id)
{ {
for_id = for_widget.getInputNode().id; for_id = for_widget.getInputNode().id;
} }

View File

@ -96,14 +96,14 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
description: "Defines sortable start callback function" description: "Defines sortable start callback function"
} }
}; };
private table: JQuery; private table : JQuery;
private thead: JQuery; private thead : JQuery;
private tfoot: JQuery; private tfoot : JQuery;
private tbody: JQuery; private tbody : JQuery;
// Counters for rows and columns // Counters for rows and columns
private rowCount: number = 0; private rowCount : number = 0;
private columnCount: number = 0; private columnCount : number = 0;
// 2D-Array which holds references to the DOM td tags // 2D-Array which holds references to the DOM td tags
private cells = []; private cells = [];
@ -116,9 +116,10 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
// Wrapper div for height & overflow, if needed // Wrapper div for height & overflow, if needed
private wrapper = null; private wrapper = null;
private lastRowNode: null; private lastRowNode : null;
private sortablejs : Sortable = null; private sortablejs : Sortable = null;
/** /**
* Constructor * Constructor
* *
@ -139,19 +140,20 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
.appendTo(this.table); .appendTo(this.table);
} }
_initCells(_colData : ColumnEntry[], _rowData : RowEntry[]) { _initCells(_colData : ColumnEntry[], _rowData : RowEntry[])
{
// Copy the width and height // Copy the width and height
const w = _colData.length; const w = _colData.length;
const h = _rowData.length; const h = _rowData.length;
// Create the 2D-Cells array // Create the 2D-Cells array
const cells = new Array(h); const cells = new Array(h);
for (let y = 0; y < h; y++) for(let y = 0; y < h; y++)
{ {
cells[y] = new Array(w); cells[y] = new Array(w);
// Initialize the cell description objects // Initialize the cell description objects
for (let x = 0; x < w; x++) for(let x = 0; x < w; x++)
{ {
// Some columns (nm) we do not parse into a boolean // Some columns (nm) we do not parse into a boolean
const col_disabled = _colData[x].disabled; const col_disabled = _colData[x].disabled;
@ -178,7 +180,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
_getColDataEntry() : ColumnEntry _getColDataEntry() : ColumnEntry
{ {
return { return {
width: "auto", width: "auto",
class: "", class: "",
align: "", align: "",
@ -200,10 +202,10 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
_getCell(_cells, _x, _y) _getCell(_cells, _x, _y)
{ {
if ((0 <= _y) && (_y < _cells.length)) if((0 <= _y) && (_y < _cells.length))
{ {
const row = _cells[_y]; const row = _cells[_y];
if ((0 <= _x) && (_x < row.length)) if((0 <= _x) && (_x < row.length))
{ {
return row[_x]; return row[_x];
} }
@ -214,7 +216,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
_forceNumber(_val) _forceNumber(_val)
{ {
if (isNaN(_val)) if(isNaN(_val))
{ {
throw(_val + " is not a number!"); throw(_val + " is not a number!");
} }
@ -226,7 +228,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
{ {
// Some things cannot be done inside a nextmatch - nm will do the expansion later // Some things cannot be done inside a nextmatch - nm will do the expansion later
var nm = false; var nm = false;
let widget: et2_widget = this; let widget : et2_widget = this;
while(!nm && widget != this.getRoot()) while(!nm && widget != this.getRoot())
{ {
nm = (widget.getType() == 'nextmatch'); nm = (widget.getType() == 'nextmatch');
@ -234,14 +236,15 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
} }
// Parse the columns tag // Parse the columns tag
et2_filteredNodeIterator(columns, function(node, nodeName) { et2_filteredNodeIterator(columns, function(node, nodeName)
{
const colDataEntry = this._getColDataEntry(); const colDataEntry = this._getColDataEntry();
// This cannot be done inside a nm, it will expand it later // This cannot be done inside a nm, it will expand it later
colDataEntry["disabled"] = nm ? colDataEntry["disabled"] = nm ?
et2_readAttrWithDefault(node, "disabled", "") : et2_readAttrWithDefault(node, "disabled", "") :
this.getArrayMgr("content") this.getArrayMgr("content")
.parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", ""));
if (nodeName == "column") if(nodeName == "column")
{ {
colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto"); colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto");
colDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); colDataEntry["class"] = et2_readAttrWithDefault(node, "class", "");
@ -266,11 +269,12 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
}, this); }, this);
// Parse the rows tag // Parse the rows tag
et2_filteredNodeIterator(rows, function(node, nodeName) { et2_filteredNodeIterator(rows, function(node, nodeName)
{
const rowDataEntry = this._getRowDataEntry(); const rowDataEntry = this._getRowDataEntry();
rowDataEntry["disabled"] = this.getArrayMgr("content") rowDataEntry["disabled"] = this.getArrayMgr("content")
.parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", ""));
if (nodeName == "row") if(nodeName == "row")
{ {
// Remember this row for auto-repeat - it'll eventually be the last one // Remember this row for auto-repeat - it'll eventually be the last one
this.lastRowNode = node; this.lastRowNode = node;
@ -300,8 +304,8 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
if(this.getArrayMgr("content")) if(this.getArrayMgr("content"))
{ {
const content = this.getArrayMgr("content"); const content = this.getArrayMgr("content");
var rowDataEntry = rowData[rowData.length-1]; var rowDataEntry = rowData[rowData.length - 1];
rowIndex = rowData.length-1; rowIndex = rowData.length - 1;
// Find out if we have any content rows, and how many // Find out if we have any content rows, and how many
let cont = true; let cont = true;
while(cont) while(cont)
@ -313,7 +317,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
rowIndex++; rowIndex++;
} }
else if (this.lastRowNode != null) else if(this.lastRowNode != null)
{ {
// Have to look through actual widgets to support const[$row] // Have to look through actual widgets to support const[$row]
// style names - should be avoided so we can remove this extra check // style names - should be avoided so we can remove this extra check
@ -328,41 +332,49 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
// Not in a nextmatch, so we can expand with abandon // Not in a nextmatch, so we can expand with abandon
const currentPerspective = jQuery.extend({}, content.perspectiveData); const currentPerspective = jQuery.extend({}, content.perspectiveData);
const check = function (node, nodeName) { const check = function(node, nodeName)
if (nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox') { {
if(nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox')
{
return et2_filteredNodeIterator(node, check, this); return et2_filteredNodeIterator(node, check, this);
} }
content.perspectiveData.row = rowIndex; content.perspectiveData.row = rowIndex;
for (let attr in node.attributes) { for(let attr in node.attributes)
{
const value = et2_readAttrWithDefault(node, node.attributes[attr].name, ""); const value = et2_readAttrWithDefault(node, node.attributes[attr].name, "");
// Don't include first char, those should be handled by normal means // Don't include first char, those should be handled by normal means
// and it would break nextmatch // and it would break nextmatch
if (value.indexOf('@') > 0 || value.indexOf('$') > 0) { if(value.indexOf('@') > 0 || value.indexOf('$') > 0)
{
// Ok, we found something. How many? Check for values. // Ok, we found something. How many? Check for values.
let ident = content.expandName(value); let ident = content.expandName(value);
// expandName() handles index into content (@), but we have to look up // expandName() handles index into content (@), but we have to look up
// regular values // regular values
if (value[0] != '@') { if(value[0] != '@')
{
// Returns null if there isn't an actual value // Returns null if there isn't an actual value
ident = content.getEntry(ident, false, true); ident = content.getEntry(ident, false, true);
} }
while (ident != null && rowIndex < 1000) { while(ident != null && rowIndex < 1000)
{
rowData[rowIndex] = jQuery.extend({}, rowDataEntry); rowData[rowIndex] = jQuery.extend({}, rowDataEntry);
content.perspectiveData.row = ++rowIndex; content.perspectiveData.row = ++rowIndex;
ident = content.expandName(value); ident = content.expandName(value);
if (value[0] != '@') { if(value[0] != '@')
{
// Returns null if there isn't an actual value // Returns null if there isn't an actual value
ident = content.getEntry(ident, false, true); ident = content.getEntry(ident, false, true);
} }
} }
if (rowIndex >= 1000) { if(rowIndex >= 1000)
{
egw.debug("error", "Problem in autorepeat fallback: too many rows for '%s'. Use a nextmatch, or start debugging.", value); egw.debug("error", "Problem in autorepeat fallback: too many rows for '%s'. Use a nextmatch, or start debugging.", value);
} }
return; return;
} }
} }
}; };
et2_filteredNodeIterator(this.lastRowNode, check,this); et2_filteredNodeIterator(this.lastRowNode, check, this);
cont = false; cont = false;
content.perspectiveData = currentPerspective; content.perspectiveData = currentPerspective;
} }
@ -381,7 +393,8 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
} }
} }
_fillCells(cells, columns : object[], rows : object[]) { _fillCells(cells, columns : object[], rows : object[])
{
const h = cells.length; const h = cells.length;
const w = (h > 0) ? cells[0].length : 0; const w = (h > 0) ? cells[0].length : 0;
const currentPerspective = jQuery.extend({}, this.getArrayMgr("content").perspectiveData); const currentPerspective = jQuery.extend({}, this.getArrayMgr("content").perspectiveData);
@ -389,10 +402,12 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
// Read the elements inside the columns // Read the elements inside the columns
let x = 0; let x = 0;
et2_filteredNodeIterator(columns, function(node, nodeName) { et2_filteredNodeIterator(columns, function(node, nodeName)
{
function _readColNode(node, nodeName) { function _readColNode(node, nodeName)
if (y >= h) {
if(y >= h)
{ {
this.egw().debug("warn", "Skipped grid cell in column, '" + this.egw().debug("warn", "Skipped grid cell in column, '" +
nodeName + "'"); nodeName + "'");
@ -402,7 +417,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const cell = this._getCell(cells, x, y); const cell = this._getCell(cells, x, y);
// Read the span value of the element // Read the span value of the element
if (node.getAttribute("span")) if(node.getAttribute("span"))
{ {
cell.rowSpan = node.getAttribute("span"); cell.rowSpan = node.getAttribute("span");
} }
@ -412,7 +427,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
cell.autoRowSpan = true; cell.autoRowSpan = true;
} }
if (cell.rowSpan == "all") if(cell.rowSpan == "all")
{ {
cell.rowSpan = cells.length; cell.rowSpan = cells.length;
} }
@ -423,7 +438,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const widget = this.createElementFromNode(node, nodeName); const widget = this.createElementFromNode(node, nodeName);
// Fill all cells the widget is spanning // Fill all cells the widget is spanning
for (let i = 0; i < span && y < cells.length; i++, y++) for(let i = 0; i < span && y < cells.length; i++, y++)
{ {
this._getCell(cells, x, y).widget = widget; this._getCell(cells, x, y).widget = widget;
} }
@ -432,7 +447,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
// If the node is a column, create the widgets which belong into // If the node is a column, create the widgets which belong into
// the column // the column
var y = 0; var y = 0;
if (nodeName == "column") if(nodeName == "column")
{ {
et2_filteredNodeIterator(node, _readColNode, this); et2_filteredNodeIterator(node, _readColNode, this);
} }
@ -455,10 +470,12 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
nm = (widget.getType() == 'nextmatch'); nm = (widget.getType() == 'nextmatch');
widget = widget.getParent(); widget = widget.getParent();
} }
et2_filteredNodeIterator(rows, function(node, nodeName) { et2_filteredNodeIterator(rows, function(node, nodeName)
{
readRowNode = function _readRowNode(node, nodeName) { readRowNode = function _readRowNode(node, nodeName)
if (x >= w) {
if(x >= w)
{ {
if(nodeName != "description") if(nodeName != "description")
{ {
@ -473,7 +490,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
let cell = this._getCell(cells, x, y); let cell = this._getCell(cells, x, y);
// Read the span value of the element // Read the span value of the element
if (node.getAttribute("span")) if(node.getAttribute("span"))
{ {
cell.colSpan = node.getAttribute("span"); cell.colSpan = node.getAttribute("span");
} }
@ -483,7 +500,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
cell.autoColSpan = true; cell.autoColSpan = true;
} }
if (cell.colSpan == "all") if(cell.colSpan == "all")
{ {
cell.colSpan = cells[y].length; cell.colSpan = cells[y].length;
} }
@ -491,13 +508,13 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const span = cell.colSpan = this._forceNumber(cell.colSpan); const span = cell.colSpan = this._forceNumber(cell.colSpan);
// Read the align value of the element // Read the align value of the element
if (node.getAttribute("align")) if(node.getAttribute("align"))
{ {
cell.align = node.getAttribute("align"); cell.align = node.getAttribute("align");
} }
// store id of nextmatch-*headers, so it is available for disabled widgets, which get not instanciated // store id of nextmatch-*headers, so it is available for disabled widgets, which get not instanciated
if (nodeName.substr(0, 10) == 'nextmatch-') if(nodeName.substr(0, 10) == 'nextmatch-')
{ {
cell.nm_id = node.getAttribute('id'); cell.nm_id = node.getAttribute('id');
} }
@ -540,10 +557,10 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
} }
// Fill all cells the widget is spanning // Fill all cells the widget is spanning
for (let i = 0; i < span && x < cells[y].length; i++, x++) for(let i = 0; i < span && x < cells[y].length; i++, x++)
{ {
cell = this._getCell(cells, x, y); cell = this._getCell(cells, x, y);
if (cell.widget == null) if(cell.widget == null)
{ {
cell.widget = widget; cell.widget = widget;
} }
@ -562,7 +579,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
{ {
return; return;
} }
if (nodeName == "row") if(nodeName == "row")
{ {
// Adjust for the row // Adjust for the row
for(var name in this.getArrayMgrs()) for(var name in this.getArrayMgrs())
@ -570,7 +587,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
//this.getArrayMgr(name).perspectiveData.row = y; //this.getArrayMgr(name).perspectiveData.row = y;
} }
let cell = this._getCell(cells, x,y); let cell = this._getCell(cells, x, y);
if(cell.rowData.id) if(cell.rowData.id)
{ {
this.getArrayMgr("content").expandName(cell.rowData.id); this.getArrayMgr("content").expandName(cell.rowData.id);
@ -595,7 +612,8 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
}, this); }, this);
// Extra content rows // Extra content rows
for(y; y < h; y++) { for(y; y < h; y++)
{
x = 0; x = 0;
et2_filteredNodeIterator(this.lastRowNode, readRowNode, this); et2_filteredNodeIterator(this.lastRowNode, readRowNode, this);
@ -614,15 +632,15 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
// Determine the last cell in each row and expand its span value if // Determine the last cell in each row and expand its span value if
// the span has not been explicitly set. // the span has not been explicitly set.
for (var y = 0; y < h; y++) for(var y = 0; y < h; y++)
{ {
for (var x = w - 1; x >= 0; x--) for(var x = w - 1; x >= 0; x--)
{ {
var cell = _cells[y][x]; var cell = _cells[y][x];
if (cell.widget != null) if(cell.widget != null)
{ {
if (cell.autoColSpan) if(cell.autoColSpan)
{ {
cell.colSpan = w - x; cell.colSpan = w - x;
} }
@ -633,15 +651,15 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
// Determine the last cell in each column and expand its span value if // Determine the last cell in each column and expand its span value if
// the span has not been explicitly set. // the span has not been explicitly set.
for (var x = 0; x < w; x++) for(var x = 0; x < w; x++)
{ {
for (var y = h - 1; y >= 0; y--) for(var y = h - 1; y >= 0; y--)
{ {
var cell = _cells[y][x]; var cell = _cells[y][x];
if (cell.widget != null) if(cell.widget != null)
{ {
if (cell.autoRowSpan) if(cell.autoRowSpan)
{ {
cell.rowSpan = h - y; cell.rowSpan = h - y;
} }
@ -655,6 +673,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
{ {
return true; return true;
} }
/** /**
* As the does not fit very well into the default widget structure, we're * As the does not fit very well into the default widget structure, we're
* overwriting the loadFromXML function and doing a two-pass reading - * overwriting the loadFromXML function and doing a two-pass reading -
@ -662,7 +681,8 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
* *
* @param {object} _node xml node to process * @param {object} _node xml node to process
*/ */
loadFromXML(_node ) { loadFromXML(_node)
{
// Keep the node for later changing / reloading // Keep the node for later changing / reloading
this.template_node = _node; this.template_node = _node;
@ -670,12 +690,12 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const rowsElems = et2_directChildrenByTagName(_node, "rows"); const rowsElems = et2_directChildrenByTagName(_node, "rows");
const columnsElems = et2_directChildrenByTagName(_node, "columns"); const columnsElems = et2_directChildrenByTagName(_node, "columns");
if (rowsElems.length == 1 && columnsElems.length == 1) if(rowsElems.length == 1 && columnsElems.length == 1)
{ {
const columns = columnsElems[0]; const columns = columnsElems[0];
const rows = rowsElems[0]; const rows = rowsElems[0];
const colData: ColumnEntry[] = []; const colData : ColumnEntry[] = [];
const rowData: RowEntry[] = []; const rowData : RowEntry[] = [];
// Fetch the column and row data // Fetch the column and row data
this._fetchRowColData(columns, rows, colData, rowData); this._fetchRowColData(columns, rows, colData, rowData);
@ -698,7 +718,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
} }
} }
createTableFromCells(_cells, _colData: any[], _rowData: any[]) createTableFromCells(_cells, _colData : any[], _rowData : any[])
{ {
this.managementArray = []; this.managementArray = [];
this.cells = _cells; this.cells = _cells;
@ -710,19 +730,19 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const w = this.columnCount = (h > 0) ? _cells[0].length : 0; const w = this.columnCount = (h > 0) ? _cells[0].length : 0;
// Create the table rows. // Create the table rows.
for (let y = 0; y < h; y++) for(let y = 0; y < h; y++)
{ {
let parent = this.tbody; let parent = this.tbody;
switch(this.rowData[y]["part"]) switch(this.rowData[y]["part"])
{ {
case 'header': case 'header':
if (!this.tbody.children().length && !this.tfoot.children().length) if(!this.tbody.children().length && !this.tfoot.children().length)
{ {
parent = this.thead; parent = this.thead;
} }
break; break;
case 'footer': case 'footer':
if (!this.tbody.children().length) if(!this.tbody.children().length)
{ {
parent = this.tfoot; parent = this.tfoot;
} }
@ -731,17 +751,17 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const tr = jQuery(document.createElement("tr")).appendTo(parent) const tr = jQuery(document.createElement("tr")).appendTo(parent)
.addClass(this.rowData[y]["class"]); .addClass(this.rowData[y]["class"]);
if (this.rowData[y].disabled) if(this.rowData[y].disabled)
{ {
tr.hide(); tr.hide();
} }
if (this.rowData[y].height != "auto") if(this.rowData[y].height != "auto")
{ {
tr.height(this.rowData[y].height); tr.height(this.rowData[y].height);
} }
if (this.rowData[y].valign) if(this.rowData[y].valign)
{ {
tr.attr("valign", this.rowData[y].valign); tr.attr("valign", this.rowData[y].valign);
} }
@ -752,31 +772,39 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
} }
// Create the cells. x is incremented by the colSpan value of the // Create the cells. x is incremented by the colSpan value of the
// cell. // cell.
for (let x = 0; x < w;) for(let x = 0; x < w;)
{ {
// Fetch a cell from the cells // Fetch a cell from the cells
const cell = this._getCell(_cells, x, y); const cell = this._getCell(_cells, x, y);
if (cell.td == null && cell.widget != null) if(cell.td == null && cell.widget != null)
{ {
// Create the cell // Create the cell
const td = jQuery(document.createElement("td")).appendTo(tr) const td = jQuery(document.createElement("td")).appendTo(tr)
.addClass(cell["class"]); .addClass(cell["class"]);
if (cell.disabled) if(cell.disabled)
{ {
td.hide(); td.hide();
cell.widget.options.disabled = cell.disabled; // Need to do different things with webComponents
if(typeof cell.widget.options !== "undefined")
{
cell.widget.options.disabled = cell.disabled;
}
else if(cell.widget.nodeName)
{
cell.widget.disabled = cell.disabled;
}
} }
if (cell.width != "auto") if(cell.width != "auto")
{ {
td.width(cell.width); td.width(cell.width);
} }
if (cell.align) if(cell.align)
{ {
td.attr("align",cell.align); td.attr("align", cell.align);
} }
// Add the entry for the widget to the management array // Add the entry for the widget to the management array
@ -791,18 +819,20 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const rs = (y == h - 1) ? h - y : Math.min(h - y, cell.rowSpan); const rs = (y == h - 1) ? h - y : Math.min(h - y, cell.rowSpan);
// Set the col and row span values // Set the col and row span values
if (cs > 1) { if(cs > 1)
{
td.attr("colspan", cs); td.attr("colspan", cs);
} }
if (rs > 1) { if(rs > 1)
{
td.attr("rowspan", rs); td.attr("rowspan", rs);
} }
// Assign the td to the cell // Assign the td to the cell
for (let sx = x; sx < x + cs; sx++) for(let sx = x; sx < x + cs; sx++)
{ {
for (let sy = y; sy < y + rs; sy++) for(let sy = y; sy < y + rs; sy++)
{ {
this._getCell(_cells, sx, sy).td = td; this._getCell(_cells, sx, sy).td = td;
} }
@ -822,15 +852,15 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
{ {
// If the parent class functions are asking for the DOM-Node, return the // If the parent class functions are asking for the DOM-Node, return the
// outer table. // outer table.
if (_sender == this || typeof _sender == 'undefined') if(_sender == this || typeof _sender == 'undefined')
{ {
return this.wrapper != null ? this.wrapper[0] : this.table[0]; return this.wrapper != null ? this.wrapper[0] : this.table[0];
} }
// Check whether the _sender object exists inside the management array // Check whether the _sender object exists inside the management array
for (let i = 0; i < this.managementArray.length; i++) for(let i = 0; i < this.managementArray.length; i++)
{ {
if (this.managementArray[i].widget == _sender) if(this.managementArray[i].widget == _sender)
{ {
return this.managementArray[i].cell; return this.managementArray[i].cell;
} }
@ -839,22 +869,22 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
return null; return null;
} }
isInTree(_sender?:et2_widget) : boolean isInTree(_sender? : et2_widget) : boolean
{ {
let vis = true; let vis = true;
if (typeof _sender != "undefined" && _sender != this) if(typeof _sender != "undefined" && _sender != this)
{ {
vis = false; vis = false;
// Check whether the _sender object exists inside the management array // Check whether the _sender object exists inside the management array
for (let i = 0; i < this.managementArray.length; i++) for(let i = 0; i < this.managementArray.length; i++)
{ {
if (this.managementArray[i].widget == _sender) if(this.managementArray[i].widget == _sender)
{ {
vis = !(typeof this.managementArray[i].disabled === 'boolean' ? vis = !(typeof this.managementArray[i].disabled === 'boolean' ?
this.managementArray[i].disabled : this.managementArray[i].disabled :
this.getArrayMgr("content").parseBoolExpression(this.managementArray[i].disabled) this.getArrayMgr("content").parseBoolExpression(this.managementArray[i].disabled)
); );
break; break;
} }
@ -873,7 +903,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
* *
* @param {string} _value Overflow value, must be a valid CSS overflow value, default 'visible' * @param {string} _value Overflow value, must be a valid CSS overflow value, default 'visible'
*/ */
set_overflow(_value: string) set_overflow(_value : string)
{ {
let wrapper = this.wrapper || this.table.parent('[id$="_grid_wrapper"]'); let wrapper = this.wrapper || this.table.parent('[id$="_grid_wrapper"]');
@ -881,7 +911,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
if(wrapper.length == 0 && _value && _value !== 'visible') if(wrapper.length == 0 && _value && _value !== 'visible')
{ {
this.wrapper = wrapper = this.table.wrap('<div id="'+this.id+'_grid_wrapper"></div>').parent(); this.wrapper = wrapper = this.table.wrap('<div id="' + this.id + '_grid_wrapper"></div>').parent();
if(this.height) if(this.height)
{ {
wrapper.css('height', this.height); wrapper.css('height', this.height);
@ -889,7 +919,8 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
} }
wrapper.css('overflow', _value); wrapper.css('overflow', _value);
if(wrapper.length && (!_value || _value === 'visible')) { if(wrapper.length && (!_value || _value === 'visible'))
{
this.table.unwrap(); this.table.unwrap();
} }
} }
@ -906,7 +937,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
* @param {Object} [_value.sel_options] New select options * @param {Object} [_value.sel_options] New select options
* @param {Object} [_value.readonlys] New read-only values * @param {Object} [_value.readonlys] New read-only values
*/ */
set_value(_value : {content? : object, sel_options? : object, readonlys? : object}) set_value(_value : { content? : object, sel_options? : object, readonlys? : object })
{ {
// Destroy children, empty grid // Destroy children, empty grid
for(let i = 0; i < this.managementArray.length; i++) for(let i = 0; i < this.managementArray.length; i++)
@ -914,6 +945,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
const cell = this.managementArray[i]; const cell = this.managementArray[i];
if(cell.widget) if(cell.widget)
{ {
this.removeChild(cell.widget);
cell.widget.destroy(); cell.widget.destroy();
} }
} }
@ -943,7 +975,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
* *
* @param {boolean|function} sortable Callback or false to disable * @param {boolean|function} sortable Callback or false to disable
*/ */
set_sortable(sortable: boolean | Function) set_sortable(sortable : boolean | Function)
{ {
const self = this; const self = this;
let tbody = this.getDOMNode().getElementsByTagName('tbody')[0]; let tbody = this.getDOMNode().getElementsByTagName('tbody')[0];
@ -954,32 +986,37 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
return; return;
} }
for (let i =0; i < tbody.children.length; i++) for(let i = 0; i < tbody.children.length; i++)
{ {
if (!tbody.children[i].classList.contains('th') && !tbody.children[i].id) if(!tbody.children[i].classList.contains('th') && !tbody.children[i].id)
{ {
tbody.children[i].setAttribute('id', i.toString()); tbody.children[i].setAttribute('id', i.toString());
} }
} }
this.sortablejs = new Sortable(tbody,{ this.sortablejs = new Sortable(tbody, {
group: this.options.sortable_connectWith, group: this.options.sortable_connectWith,
draggable: "tr:not(.th)", draggable: "tr:not(.th)",
filter: this.options.sortable_cancel, filter: this.options.sortable_cancel,
ghostClass: this.options.sortable_placeholder, ghostClass: this.options.sortable_placeholder,
dataIdAttr: 'id', dataIdAttr: 'id',
onAdd:function (event) { onAdd: function(event)
if (typeof self.options.sortable_recieveCallback == 'function') { {
if(typeof self.options.sortable_recieveCallback == 'function')
{
self.options.sortable_recieveCallback.call(self, event, this, self.id); self.options.sortable_recieveCallback.call(self, event, this, self.id);
} }
}, },
onStart: function (event, ui) { onStart: function(event, ui)
if (typeof self.options.sortable_startCallback == 'function') { {
if(typeof self.options.sortable_startCallback == 'function')
{
self.options.sortable_startCallback.call(self, event, this, self.id); self.options.sortable_startCallback.call(self, event, this, self.id);
} }
}, },
onSort: function (event) { onSort: function(event)
self.egw().json(sortable,[ {
self.egw().json(sortable, [
self.getInstanceManager().etemplate_exec_id, self.getInstanceManager().etemplate_exec_id,
self.sortablejs.toArray(), self.sortablejs.toArray(),
self.id], self.id],
@ -998,13 +1035,14 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
*/ */
_link_actions(actions : object[]) _link_actions(actions : object[])
{ {
// Get the top level element for the tree // Get the top level element for the tree
// get appObjectManager for the actual app, it might not always be the current app(e.g. running app content under admin tab) // get appObjectManager for the actual app, it might not always be the current app(e.g. running app content under admin tab)
// @ts-ignore // @ts-ignore
let objectManager = egw_getAppObjectManager(true, this.getInstanceManager().app); let objectManager = egw_getAppObjectManager(true, this.getInstanceManager().app);
objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager; objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager;
let widget_object = objectManager.getObjectById(this.id); let widget_object = objectManager.getObjectById(this.id);
if (widget_object == null) { if(widget_object == null)
{
// Add a new container to the object manager which will hold the widget // Add a new container to the object manager which will hold the widget
// objects // objects
widget_object = objectManager.insertObject(false, new egwActionObject( widget_object = objectManager.insertObject(false, new egwActionObject(
@ -1024,7 +1062,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
let i = 0, r = 0; let i = 0, r = 0;
for(; i < this.rowData.length; i++) for(; i < this.rowData.length; i++)
{ {
if (this.rowData[i].part != 'body') continue; if(this.rowData[i].part != 'body') continue;
const content = this.getArrayMgr('content').getEntry(i); const content = this.getArrayMgr('content').getEntry(i);
if(content) if(content)
{ {
@ -1073,33 +1111,33 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
_getColumnName() _getColumnName()
{ {
const ids = []; const ids = [];
for(let r=0; r < this.cells.length; ++r) for(let r = 0; r < this.cells.length; ++r)
{ {
const cols = this.cells[r]; const cols = this.cells[r];
for(let c=0; c < cols.length; ++c) for(let c = 0; c < cols.length; ++c)
{ {
if (cols[c].nm_id) ids.push(cols[c].nm_id); if(cols[c].nm_id) ids.push(cols[c].nm_id);
} }
} }
return ids.join('_'); return ids.join('_');
} }
resize (_height) resize(_height)
{ {
if (typeof this.options != 'undefined' && _height if(typeof this.options != 'undefined' && _height
&& typeof this.options.resize_ratio != 'undefined' && this.options.resize_ratio) && typeof this.options.resize_ratio != 'undefined' && this.options.resize_ratio)
{ {
// apply the ratio // apply the ratio
_height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height;
if (_height != 0) if(_height != 0)
{ {
if (this.wrapper) if(this.wrapper)
{ {
this.wrapper.height(this.wrapper.height() + _height); this.wrapper.height(this.wrapper.height() + _height);
} }
else else
{ {
this.table.height(this.table.height() + _height ); this.table.height(this.table.height() + _height);
} }
} }
@ -1119,28 +1157,32 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
*/ */
getRow(_sender : et2_widget) : et2_widget getRow(_sender : et2_widget) : et2_widget
{ {
if (!_sender || !this.cells) return; if(!_sender || !this.cells) return;
for(let r=0; r < this.cells.length; ++r) for(let r = 0; r < this.cells.length; ++r)
{ {
const row = this.cells[r]; const row = this.cells[r];
for(var c=0; c < row.length; ++c) for(var c = 0; c < row.length; ++c)
{ {
if (!row[c].widget) continue; if(!row[c].widget) continue;
let found = row[c].widget === _sender; let found = row[c].widget === _sender;
if (!found) row[c].widget.iterateOver(function(_widget) {if (_widget === _sender) found = true;}); if(!found) row[c].widget.iterateOver(function(_widget)
if (found) {
if(_widget === _sender) found = true;
});
if(found)
{ {
// return a fake row object allowing to iterate over it's children // return a fake row object allowing to iterate over it's children
const row_obj: et2_widget = new et2_widget(this, {}); const row_obj : et2_widget = new et2_widget(this, {});
for(var c=0; c < row.length; ++c) for(var c = 0; c < row.length; ++c)
{ {
if (row[c].widget) row_obj.addChild(row[c].widget); if(row[c].widget) row_obj.addChild(row[c].widget);
} }
row_obj.isInTree = jQuery.proxy(this.isInTree, this); row_obj.isInTree = jQuery.proxy(this.isInTree, this);
// we must not free the children! // we must not free the children!
row_obj.destroy = function(){ row_obj.destroy = function()
{
// @ts-ignore // @ts-ignore
delete row_obj._children; delete row_obj._children;
}; };
@ -1153,28 +1195,30 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
/** /**
* Needed for the align interface, but we're not doing anything with it... * Needed for the align interface, but we're not doing anything with it...
*/ */
get_align(): string { get_align() : string
{
return ""; return "";
} }
} }
et2_register_widget(et2_grid, ["grid"]); et2_register_widget(et2_grid, ["grid"]);
interface ColumnEntry interface ColumnEntry
{ {
id? : string, id? : string,
width: string | number, // 'auto' width : string | number, // 'auto'
class: string, // "", class : string, // "",
align: string, // "", align : string, // "",
span: string | number // "1", span : string | number // "1",
disabled: boolean // false disabled : boolean // false
} }
interface RowEntry interface RowEntry
{ {
id? : string id? : string
height: string | number, // "auto", height : string | number, // "auto",
class: string, // "", class : string, // "",
valign: string, // "top", valign : string, // "top",
span: string | number, // "1", span : string | number, // "1",
disabled: boolean // false disabled : boolean // false
} }

View File

@ -199,6 +199,6 @@ export class et2_hbox extends et2_baseWidget
} }
} }
} }
et2_register_widget(et2_hbox, ["hbox"]); et2_register_widget(et2_hbox, ["old-hbox"]);

View File

@ -23,7 +23,12 @@ import {et2_nextmatch, et2_nextmatch_header_bar} from "./et2_extension_nextmatch
import {et2_tabbox} from "./et2_widget_tabs"; import {et2_tabbox} from "./et2_widget_tabs";
import '../jsapi/egw_json.js'; import '../jsapi/egw_json.js';
import {egwIsMobile} from "../egw_action/egw_action_common.js"; import {egwIsMobile} from "../egw_action/egw_action_common.js";
//import './et2-button'; import './Et2Box/Et2Box';
import './Et2Button/Et2Button';
import './Et2Date/Et2DateTime';
import './Et2Textarea/Et2Textarea';
import './Et2Textbox/Et2Textbox';
import './Et2Colorpicker/Et2Colorpicker';
/* Include all widget classes here, we only care about them registering, not importing anything*/ /* Include all widget classes here, we only care about them registering, not importing anything*/
import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle
import './et2_widget_template'; import './et2_widget_template';
@ -113,7 +118,7 @@ export class etemplate2
private app_obj: EgwApp; private app_obj: EgwApp;
app: string; app: string;
constructor(_container : HTMLElement, _menuaction? : string, _uniqueId?: string) constructor(_container: HTMLElement, _menuaction?: string, _uniqueId?: string)
{ {
if (typeof _menuaction == "undefined") if (typeof _menuaction == "undefined")
{ {
@ -165,13 +170,15 @@ export class etemplate2
* *
* @param {jQuery.event} e * @param {jQuery.event} e
*/ */
public resize(e) { public resize(e)
{
const event = e; const event = e;
const self = this; const self = this;
let excess_height: number | boolean = false; let excess_height: number | boolean = false;
// Check if the framework has an specific excess height calculation // Check if the framework has an specific excess height calculation
if (typeof window.framework != 'undefined' && typeof window.framework.get_wExcessHeight != 'undefined') { if (typeof window.framework != 'undefined' && typeof window.framework.get_wExcessHeight != 'undefined')
{
excess_height = window.framework.get_wExcessHeight(window); excess_height = window.framework.get_wExcessHeight(window);
} }
@ -200,7 +207,7 @@ export class etemplate2
// If we're visible, call the "resize" event of all functions which implement the // If we're visible, call the "resize" event of all functions which implement the
// "IResizeable" interface // "IResizeable" interface
if(jQuery(self.DOMContainer).is(":visible")) if (jQuery(self.DOMContainer).is(":visible"))
{ {
self._widgetContainer.iterateOver(function (_widget) self._widgetContainer.iterateOver(function (_widget)
{ {
@ -227,7 +234,7 @@ export class etemplate2
* @param _keep_app_object keep app object * @param _keep_app_object keep app object
* @param _keep_session keep server-side et2 session eg. for vfs-select * @param _keep_session keep server-side et2 session eg. for vfs-select
*/ */
public clear(_keep_app_object?:boolean, _keep_session?: boolean) public clear(_keep_app_object?: boolean, _keep_session?: boolean)
{ {
jQuery(this._DOMContainer).trigger('clear'); jQuery(this._DOMContainer).trigger('clear');
@ -267,11 +274,11 @@ export class etemplate2
} }
// If using a private app object, remove all of them // If using a private app object, remove all of them
if(!_keep_app_object && this.app_obj !== window.app) if (!_keep_app_object && this.app_obj !== window.app)
{ {
for(const app_name in this.app_obj) for (const app_name in this.app_obj)
{ {
if(this.app_obj[app_name] instanceof EgwApp) if (this.app_obj[app_name] instanceof EgwApp)
{ {
this.app_obj[app_name].destroy(); this.app_obj[app_name].destroy();
} }
@ -310,7 +317,7 @@ export class etemplate2
// Create all neccessary _data entries // Create all neccessary _data entries
const neededEntries = ["content", "sel_options", "readonlys", "modifications", const neededEntries = ["content", "sel_options", "readonlys", "modifications",
"validation_errors"]; "validation_errors"];
for (let i = 0; i < neededEntries.length; i++) for (let i = 0; i < neededEntries.length; i++)
{ {
if (typeof _data[neededEntries[i]] == "undefined" || !_data[neededEntries[i]]) if (typeof _data[neededEntries[i]] == "undefined" || !_data[neededEntries[i]])
@ -353,7 +360,7 @@ export class etemplate2
bind_unload() bind_unload()
{ {
// Prompt user to save for dirty popups // Prompt user to save for dirty popups
if(window !== egw_topWindow() && !this.close_prompt) if (window !== egw_topWindow() && !this.close_prompt)
{ {
this.close_prompt = this._close_changed_prompt.bind(this); this.close_prompt = this._close_changed_prompt.bind(this);
window.addEventListener("beforeunload", this.close_prompt); window.addEventListener("beforeunload", this.close_prompt);
@ -364,22 +371,22 @@ export class etemplate2
{ {
// need to use async === "keepalive" to run via beforeunload // need to use async === "keepalive" to run via beforeunload
egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session", egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session",
[this._etemplate_exec_id], null, null, "keepalive").sendRequest(); [this._etemplate_exec_id], null, null, "keepalive").sendRequest();
}, this); }, this);
window.addEventListener("beforeunload", this.destroy_session); window.addEventListener("beforeunload", this.destroy_session);
} }
} }
private _close_changed_prompt(e : BeforeUnloadEvent) private _close_changed_prompt(e: BeforeUnloadEvent)
{ {
if(this._skip_close_prompt || !this.isDirty()) if (this._skip_close_prompt || !this.isDirty())
{ {
return; return;
} }
// Cancel the event // Cancel the event
e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
// Chrome requires returnValue to be set // Chrome requires returnValue to be set
e.returnValue = ''; e.returnValue = '';
@ -507,12 +514,15 @@ export class etemplate2
// require necessary translations from server AND the app.js file, if not already loaded // require necessary translations from server AND the app.js file, if not already loaded
let promisses = [window.egw_ready]; // to wait for legacy-loaded JS let promisses = [window.egw_ready]; // to wait for legacy-loaded JS
if (Array.isArray(_data.langRequire)) { if (Array.isArray(_data.langRequire))
{
promisses.push(egw(currentapp, window).langRequire(window, _data.langRequire)); promisses.push(egw(currentapp, window).langRequire(window, _data.langRequire));
} }
return Promise.all(promisses).catch((err) => { return Promise.all(promisses).catch((err) =>
console.log("et2.load(): error loading lang-files and app.js: "+err.message); {
}).then(() => { console.log("et2.load(): error loading lang-files and app.js: " + err.message);
}).then(() =>
{
this.clear(); this.clear();
// Initialize application js // Initialize application js
@ -564,7 +574,8 @@ export class etemplate2
const _load = function () const _load = function ()
{ {
egw.debug("log", "Loading template..."); egw.debug("log", "Loading template...");
if (egw.debug_level() >= 4 && console.timeStamp) { if (egw.debug_level() >= 4 && console.timeStamp)
{
console.timeStamp("Begin rendering template"); console.timeStamp("Begin rendering template");
} }
@ -628,11 +639,11 @@ export class etemplate2
// Date fields open the calendar popup on focus // Date fields open the calendar popup on focus
.not('.et2_date') .not('.et2_date')
.filter(function () .filter(function ()
{ {
// Skip inputs that are out of tab ordering // Skip inputs that are out of tab ordering
const $this = jQuery(this); const $this = jQuery(this);
return !$this.attr('tabindex') || parseInt($this.attr('tabIndex')) >= 0; return !$this.attr('tabindex') || parseInt($this.attr('tabIndex')) >= 0;
}).first(); }).first();
// mobile device, focus only if the field is empty (usually means new entry) // mobile device, focus only if the field is empty (usually means new entry)
// should focus always for non-mobile one // should focus always for non-mobile one
@ -670,17 +681,19 @@ export class etemplate2
// Profiling // Profiling
if (egw.debug_level() >= 4) if (egw.debug_level() >= 4)
{ {
if (console.timeEnd) { if (console.timeEnd)
{
console.timeEnd(_name); console.timeEnd(_name);
} }
if (console.profileEnd) { if (console.profileEnd)
{
console.profileEnd(_name); console.profileEnd(_name);
} }
const end_time = (new Date).getTime(); const end_time = (new Date).getTime();
let gen_time_div = jQuery('#divGenTime_' + appname); let gen_time_div = jQuery('#divGenTime_' + appname);
if (!gen_time_div.length) gen_time_div = jQuery('.pageGenTime'); if (!gen_time_div.length) gen_time_div = jQuery('.pageGenTime');
gen_time_div.find('.et2RenderTime').remove(); gen_time_div.find('.et2RenderTime').remove();
gen_time_div.append('<span class="et2RenderTime">' + egw.lang('eT2 rendering took %1s', ''+((end_time - start_time) / 1000)) + '</span>'); gen_time_div.append('<span class="et2RenderTime">' + egw.lang('eT2 rendering took %1s', '' + ((end_time - start_time) / 1000)) + '</span>');
} }
}, this)); }, this));
}; };
@ -819,7 +832,8 @@ export class etemplate2
indexes = [indexes.shift(), indexes.join('[')]; indexes = [indexes.shift(), indexes.join('[')];
indexes[1] = indexes[1].substring(0, indexes[1].length - 1); indexes[1] = indexes[1].substring(0, indexes[1].length - 1);
const children = indexes[1].split(']['); const children = indexes[1].split('][');
if (children.length) { if (children.length)
{
indexes = jQuery.merge([indexes[0]], children); indexes = jQuery.merge([indexes[0]], children);
} }
} }
@ -974,7 +988,7 @@ export class etemplate2
this.unbind_unload(); this.unbind_unload();
const form = jQuery("<form id='form' action='" + egw().webserverUrl + const form = jQuery("<form id='form' action='" + egw().webserverUrl +
"/index.php?menuaction=" + this._widgetContainer.egw().getAppName() + ".EGroupware\\Api\\Etemplate.process_exec&ajax=true' method='POST'>"); "/index.php?menuaction=" + this._widgetContainer.egw().getAppName() + ".EGroupware\\Api\\Etemplate.process_exec&ajax=true' method='POST'>");
const etemplate_id = jQuery(document.createElement("input")) const etemplate_id = jQuery(document.createElement("input"))
.attr("name", 'etemplate_exec_id') .attr("name", 'etemplate_exec_id')
@ -1009,7 +1023,8 @@ export class etemplate2
_root.iterateOver(function (_widget) _root.iterateOver(function (_widget)
{ {
// The widget must have an id to be included in the values array // The widget must have an id to be included in the values array
if (_widget.id === undefined || _widget.id === "") { if (_widget.id === undefined || _widget.id === "")
{
return; return;
} }
@ -1024,7 +1039,8 @@ export class etemplate2
indexes = [indexes.shift(), indexes.join('[')]; indexes = [indexes.shift(), indexes.join('[')];
indexes[1] = indexes[1].substring(0, indexes[1].length - 1); indexes[1] = indexes[1].substring(0, indexes[1].length - 1);
const children = indexes[1].split(']['); const children = indexes[1].split('][');
if (children.length) { if (children.length)
{
indexes = jQuery.merge([indexes[0]], children); indexes = jQuery.merge([indexes[0]], children);
} }
path = path.concat(indexes); path = path.concat(indexes);
@ -1146,7 +1162,7 @@ export class etemplate2
* @param {(string|null)} _type type of change. One of 'update','edit', 'delete', 'add' or null * @param {(string|null)} _type type of change. One of 'update','edit', 'delete', 'add' or null
* @return {boolean} true if nextmatch found and refreshed, false if not * @return {boolean} true if nextmatch found and refreshed, false if not
*/ */
static app_refresh (_msg, _app, _id, _type) static app_refresh(_msg, _app, _id, _type)
{ {
let refresh_done = false; let refresh_done = false;
let app = _app.split('-'); let app = _app.split('-');
@ -1260,7 +1276,7 @@ export class etemplate2
* @param {string} id DOM ID of the container node * @param {string} id DOM ID of the container node
* @returns {etemplate2|null} * @returns {etemplate2|null}
*/ */
public static getById (id) public static getById(id)
{ {
for (let name in etemplate2._byTemplate) for (let name in etemplate2._byTemplate)
{ {
@ -1425,38 +1441,48 @@ export class etemplate2
* @returns {Boolean} Handled by this plugin * @returns {Boolean} Handled by this plugin
* @throws Invalid parameters if the required res.data parameters are missing * @throws Invalid parameters if the required res.data parameters are missing
*/ */
public handle_assign(type, res, req) { public handle_assign(type, res, req)
{
//type, req; // unused, but required by plugin signature //type, req; // unused, but required by plugin signature
//Check whether all needed parameters have been passed and call the alertHandler function //Check whether all needed parameters have been passed and call the alertHandler function
if ((typeof res.data.id != 'undefined') && if ((typeof res.data.id != 'undefined') &&
(typeof res.data.key != 'undefined') && (typeof res.data.key != 'undefined') &&
(typeof res.data.value != 'undefined') (typeof res.data.value != 'undefined')
) { )
{
if (typeof res.data.etemplate_exec_id == 'undefined' || if (typeof res.data.etemplate_exec_id == 'undefined' ||
res.data.etemplate_exec_id != this._etemplate_exec_id) { res.data.etemplate_exec_id != this._etemplate_exec_id)
{
// Not for this etemplate, but not an error // Not for this etemplate, but not an error
return false; return false;
} }
if (res.data.key == 'etemplate_exec_id') { if (res.data.key == 'etemplate_exec_id')
{
this._etemplate_exec_id = res.data.value; this._etemplate_exec_id = res.data.value;
return true; return true;
} }
if (this._widgetContainer == null) { if (this._widgetContainer == null)
{
// Right etemplate, but it's already been cleared. // Right etemplate, but it's already been cleared.
egw.debug('warn', "Tried to call assign on an un-loaded etemplate", res.data); egw.debug('warn', "Tried to call assign on an un-loaded etemplate", res.data);
return false; return false;
} }
const widget = this._widgetContainer.getWidgetById(res.data.id); const widget = this._widgetContainer.getWidgetById(res.data.id);
if (widget) { if (widget)
if (typeof widget['set_' + res.data.key] != 'function') { {
if (typeof widget['set_' + res.data.key] != 'function')
{
egw.debug('warn', "Cannot set %s attribute %s via JSON assign, no set_%s()", res.data.id, res.data.key, res.data.key); egw.debug('warn', "Cannot set %s attribute %s via JSON assign, no set_%s()", res.data.id, res.data.key, res.data.key);
return false; return false;
} }
try { try
{
widget['set_' + res.data.key].call(widget, res.data.value); widget['set_' + res.data.key].call(widget, res.data.value);
return true; return true;
} catch (e) { }
catch (e)
{
egw.debug("error", "When assigning %s on %s via AJAX, \n" + (e.message || e + ""), res.data.key, res.data.id, widget); egw.debug("error", "When assigning %s on %s via AJAX, \n" + (e.message || e + ""), res.data.key, res.data.id, widget);
} }
} }

View File

@ -161,11 +161,11 @@ egw.extend('tooltip', egw.MODULE_WND_LOCAL, function(_app, _wnd)
* It is important to remove all tooltips from all elements which are * It is important to remove all tooltips from all elements which are
* no longer needed, in order to prevent memory leaks. * no longer needed, in order to prevent memory leaks.
* *
* @param _elem is the element to which the tooltip should get bound. It * @param _elem is the element to which the tooltip should get bound.
* has to be a jQuery node.
* @param _html is the html code which should be shown as tooltip. * @param _html is the html code which should be shown as tooltip.
*/ */
tooltipBind: function(_elem, _html, _isHtml) { tooltipBind: function(_elem, _html, _isHtml) {
_elem = jQuery(_elem);
if (_html != '') if (_html != '')
{ {
_elem.bind('mouseenter.tooltip', function(e) { _elem.bind('mouseenter.tooltip', function(e) {
@ -222,6 +222,7 @@ egw.extend('tooltip', egw.MODULE_WND_LOCAL, function(_app, _wnd)
* removed. _elem has to be a jQuery node. * removed. _elem has to be a jQuery node.
*/ */
tooltipUnbind: function(_elem) { tooltipUnbind: function(_elem) {
_elem = jQuery(_elem);
if (current_elem == _elem) if (current_elem == _elem)
{ {
hide(); hide();

View File

@ -76,4 +76,4 @@ class Button extends Etemplate\Widget
} }
} }
} }
Etemplate\Widget::registerWidget(__NAMESPACE__.'\\Button', array('button','buttononly')); Etemplate\Widget::registerWidget(__NAMESPACE__.'\\Button', array('et2-button','button','buttononly'));

View File

@ -60,7 +60,7 @@ class Date extends Transformer
* @param string $cname * @param string $cname
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
*/ */
public function beforeSendToClient($cname, array $expand=null) public function beforeSendToClient($cname, array $expand = null)
{ {
if($this->type == 'date-houronly') if($this->type == 'date-houronly')
{ {
@ -87,14 +87,20 @@ class Date extends Transformer
* @param array $expand * @param array $expand
* @param array $data Row data * @param array $data Row data
*/ */
public function set_row_value($cname, Array $expand, Array &$data) public function set_row_value($cname, array $expand, array &$data)
{ {
if($this->type == 'date-duration') return; if($this->type == 'date-duration')
{
return;
}
$form_name = self::form_name($cname, $this->id, $expand); $form_name = self::form_name($cname, $this->id, $expand);
$value =& $this->get_array($data, $form_name, true); $value =& $this->get_array($data, $form_name, true);
if (true) $value = $this->format_date($value); if(true)
{
$value = $this->format_date($value);
}
} }
/** /**
@ -104,10 +110,13 @@ class Date extends Transformer
*/ */
public function format_date($value) public function format_date($value)
{ {
if (!$value) return $value; // otherwise we will get current date or 1970-01-01 instead of an empty value if(!$value)
{
return $value;
} // otherwise we will get current date or 1970-01-01 instead of an empty value
// for DateTime objects (regular PHP and Api\DateTime ones), set user timezone // for DateTime objects (regular PHP and Api\DateTime ones), set user timezone
if ($value instanceof \DateTime) if($value instanceof \DateTime)
{ {
$date = Api\DateTime::server2user($value); $date = Api\DateTime::server2user($value);
} }
@ -142,58 +151,65 @@ class Date extends Transformer
* @param string $cname current namespace * @param string $cname current namespace
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
* @param array $content * @param array $content
* @param array &$validated=array() validated content * @param array &$validated =array() validated content
* @return boolean true if no validation error, false otherwise * @return boolean true if no validation error, false otherwise
*/ */
public function validate($cname, array $expand, array $content, &$validated=array()) public function validate($cname, array $expand, array $content, &$validated = array())
{ {
$form_name = self::form_name($cname, $this->id, $expand); $form_name = self::form_name($cname, $this->id, $expand);
if (!$this->is_readonly($cname, $form_name) && $this->type != 'date-since') // date-since is always readonly if(!$this->is_readonly($cname, $form_name) && $this->type != 'date-since') // date-since is always readonly
{ {
$value = self::get_array($content, $form_name); $value = self::get_array($content, $form_name);
$valid =& self::get_array($validated, $form_name, true); $valid =& self::get_array($validated, $form_name, true);
if ($value && $this->type !== 'date-duration') if($value && $this->type !== 'date-duration')
{ {
try try
{ {
if (substr($value, -1) === 'Z') $value = substr($value, 0, -1); if(substr($value, -1) === 'Z')
{
$value = substr($value, 0, -1);
}
$date = new Api\DateTime($value); $date = new Api\DateTime($value);
} }
catch(\Exception $e) catch (\Exception $e)
{ {
unset($e); unset($e);
$date = null; $date = null;
$value = ''; $value = '';
// this is not really a user error, but one of the clientside engine // this is not really a user error, but one of the clientside engine
self::set_validation_error($form_name,lang("'%1' is not a valid date !!!", $value).' '.$this->data_format); self::set_validation_error($form_name, lang("'%1' is not a valid date !!!", $value) . ' ' . $this->data_format);
} }
} }
if ((string)$value === '' && $this->attrs['needed']) if((string)$value === '' && $this->attrs['needed'])
{ {
self::set_validation_error($form_name,lang('Field must not be empty !!!')); self::set_validation_error($form_name, lang('Field must not be empty !!!'));
} }
elseif (is_null($value)) elseif(is_null($value))
{ {
$valid = null; $valid = null;
} }
elseif ($this->type == 'date-duration') elseif($this->type == 'date-duration')
{ {
$valid = (string)$value === '' ? '' : (int)$value; $valid = (string)$value === '' ? '' : (int)$value;
} }
if (!empty($this->attrs['min']) && !empty($value)) if(!empty($this->attrs['min']) && !empty($value))
{ {
if(is_numeric($this->attrs['min'])) if(is_numeric($this->attrs['min']))
{ {
$min = new Api\DateTime(strtotime( $this->attrs['min'] . 'days')); $min = new Api\DateTime(strtotime($this->attrs['min'] . 'days'));
} }
elseif (preg_match('/[+-][[:digit:]]+[ymwd]/',$this->attrs['min'])) elseif(preg_match('/[+-][[:digit:]]+[ymwd]/', $this->attrs['min']))
{ {
// Relative date with periods // Relative date with periods
$min = new Api\DateTime(strtotime(str_replace(array('y','m','w','d'), array('years','months','weeks','days'), $this->attrs['min']))); $min = new Api\DateTime(strtotime(str_replace(array('y', 'm', 'w', 'd'), array('years', 'months',
'weeks',
'days'), $this->attrs['min'])
)
);
} }
else else
{ {
@ -201,23 +217,28 @@ class Date extends Transformer
} }
if($date < $min) if($date < $min)
{ {
self::set_validation_error($form_name,lang( self::set_validation_error($form_name, lang(
"Value has to be at least '%1' !!!", "Value has to be at least '%1' !!!",
$min->format($this->type != 'date') $min->format($this->type != 'date')
),''); ), ''
);
$value = $min; $value = $min;
} }
} }
if (!empty($this->attrs['max']) && !empty($value)) if(!empty($this->attrs['max']) && !empty($value))
{ {
if(is_numeric($this->attrs['max'])) if(is_numeric($this->attrs['max']))
{ {
$max = new Api\DateTime(strtotime( $this->attrs['max'] . 'days')); $max = new Api\DateTime(strtotime($this->attrs['max'] . 'days'));
} }
elseif (preg_match('/[+-][[:digit:]]+[ymwd]/',$this->attrs['max'])) elseif(preg_match('/[+-][[:digit:]]+[ymwd]/', $this->attrs['max']))
{ {
// Relative date with periods // Relative date with periods
$max = new Api\DateTime(strtotime(str_replace(array('y','m','w','d'), array('years','months','weeks','days'), $this->attrs['max']))); $max = new Api\DateTime(strtotime(str_replace(array('y', 'm', 'w', 'd'), array('years', 'months',
'weeks',
'days'), $this->attrs['max'])
)
);
} }
else else
{ {
@ -225,14 +246,15 @@ class Date extends Transformer
} }
if($date > $max) if($date > $max)
{ {
self::set_validation_error($form_name,lang( self::set_validation_error($form_name, lang(
"Value has to be at maximum '%1' !!!", "Value has to be at maximum '%1' !!!",
$max->format($this->type != 'date') $max->format($this->type != 'date')
),''); ), ''
);
$value = $max; $value = $max;
} }
} }
if ($this->type == 'date-duration') if($this->type == 'date-duration')
{ {
$valid = (string)$value === '' ? '' : (int)$value; $valid = (string)$value === '' ? '' : (int)$value;
} }
@ -241,22 +263,25 @@ class Date extends Transformer
// Not null, blank // Not null, blank
$value = ''; $value = '';
} }
elseif ($date && empty($this->attrs['data_format'])) // integer timestamp elseif($date && empty($this->attrs['data_format'])) // integer timestamp
{ {
$valid = $date->format('ts'); $valid = $date->format('ts');
} }
// string with formatting letters like for php's date() method // string with formatting letters like for php's date() method
elseif ($date && ($valid = $date->format($this->attrs['data_format']))) elseif($date && ($valid = $date->format($this->attrs['data_format'])))
{ {
// Nothing to do here // Nothing to do here
} }
else else
{ {
// this is not really a user error, but one of the clientside engine // this is not really a user error, but one of the clientside engine
self::set_validation_error($form_name,lang("'%1' is not a valid date !!!", $value).' '.$this->data_format); self::set_validation_error($form_name, lang("'%1' is not a valid date !!!", $value) . ' ' . $this->data_format);
} }
//error_log("$this : ($valid)" . Api\DateTime::to($valid)); //error_log("$this : ($valid)" . Api\DateTime::to($valid));
} }
} }
} }
\EGroupware\Api\Etemplate\Widget::registerWidget(__NAMESPACE__.'\\Date', array('time_or_date'));
\EGroupware\Api\Etemplate\Widget::registerWidget(__NAMESPACE__ . '\\Date',
array('et2-date', 'et2-datetime', 'time_or_date')
);

View File

@ -205,29 +205,16 @@ class Template extends Etemplate\Widget
/** /**
* Convert relative template path from relPath to an url incl. cache-buster modification time postfix * Convert relative template path from relPath to an url incl. cache-buster modification time postfix
* *
* This adds the server-side modification of eTemplates for web-components /api/etemplate.php.
*
* @param string $path * @param string $path
* @return string url * @return string url
*/ */
public static function rel2url($path) public static function rel2url($path)
{ {
if ($path) return $GLOBALS['egw_info']['server']['webserver_url'].'/api/etemplate.php'.
{ ($path[0] === '/' ? $path : preg_replace('#^'.self::VFS_TEMPLATE_PATH.'#', '',
if ($path[0] === '/') Api\Vfs::parse_url($path, PHP_URL_PATH))).'?'.filemtime(self::rel2path($path));
{
$url = $GLOBALS['egw_info']['server']['webserver_url'].$path.'?'.filemtime(self::rel2path($path));
}
else
{
$url = Api\Vfs::download_url($path);
if ($url[0] == '/') $url = Api\Framework::link($url);
// mtime postfix has to use '?download=', as our WebDAV treats everything else literal and not ignore them like Apache for static files!
$url .= '?download='.filemtime($path);
}
}
//error_log(__METHOD__."('$path') returning $url");
return $url;
} }
/** /**
@ -299,4 +286,4 @@ if ($GLOBALS['egw_info']['flags']['debug'] == 'etemplate_widget_template')
header('Content-Type: text/xml'); header('Content-Type: text/xml');
echo $template->toXml(); echo $template->toXml();
} }
*/ */

View File

@ -179,4 +179,6 @@ class Textbox extends Etemplate\Widget
} }
} }
} }
Etemplate\Widget::registerWidget(__NAMESPACE__.'\\Textbox', array('textbox','text','int','integer','float','hidden','colorpicker','hidden')); Etemplate\Widget::registerWidget(__NAMESPACE__ . '\\Textbox', array('et2-textarea', 'et2-textbox', 'textbox', 'text',
'int', 'integer', 'float', 'hidden', 'colorpicker',
'hidden'));

View File

@ -26,6 +26,7 @@
padding: 0px; padding: 0px;
background-color: white; background-color: white;
height: 99%; height: 99%;
max-width: 100%
} }
.et2_container > div:not([class]) { .et2_container > div:not([class]) {
height: 100%; height: 100%;
@ -315,6 +316,9 @@ button.et2_button_with_image {
button.et2_button_with_image.et2_button_text { button.et2_button_with_image.et2_button_text {
background-position: 4px center; background-position: 4px center;
} }
button.et2_buttonFitContent, et2-button.et2_buttonFitContent {
max-width: fit-content;
}
/* et2_box_widget ###*/ /* et2_box_widget ###*/
button[id="cancel"], button[id="cancel"],
button#cancel { button#cancel {
@ -3387,4 +3391,4 @@ span.et2_countdown_seconds {
} }
.et2_audio audio { .et2_audio audio {
outline: none; outline: none;
} }

View File

@ -85,10 +85,8 @@
</rows> </rows>
</grid> </grid>
<vbox class="filemanager_config"> <vbox class="filemanager_config">
<button label="Mount /etemplates to allow customizing of eTemplates" id="etemplates"/> <button label="Mount /etemplates to allow customizing of eTemplates" id="etemplates" class="et2_buttonFitContent"/>
<menulist> <select id="allow_delete_versions" onchange="1" label="Who should be allowed to finally delete deleted files or old versions of a file:" empty_label="Noone" disabled="!@versioning"/>
<menupopup id="allow_delete_versions" onchange="1" label="Who should be allowed to finally delete deleted files or old versions of a file:" empty_label="Noone" disabled="!@versioning"/>
</menulist>
<hbox disabled="!@versioning"> <hbox disabled="!@versioning">
<integer id="mtime" label="Delete all older versions and deleted files older then %s days" statustext="0 means all, -N newer then N days"/> <integer id="mtime" label="Delete all older versions and deleted files older then %s days" statustext="0 means all, -N newer then N days"/>
<textbox size="30" label="under directory" id="versionedpath" statustext="/ = everywhere"/> <textbox size="30" label="under directory" id="versionedpath" statustext="/ = everywhere"/>

View File

@ -64,15 +64,17 @@ button.infologExtraButton {
height: 24px; height: 24px;
} }
button.infologExtraButton:hover { button.infologExtraButton:hover {
background-size: 16px; background-size: 16px;
} }
div.et2_hbox.tab_toolbar {
position: relative; .tab_toolbar {
position: relative;
} }
button#infolog-edit_encrypt { button#infolog-edit_encrypt {
position: absolute; position: absolute;
right: 0px; right: 0px;
top: 3px; top: 3px;
} }
td.infologTimestamp { td.infologTimestamp {
position: relative; position: relative;

View File

@ -3,7 +3,7 @@
<!-- $Id$ --> <!-- $Id$ -->
<overlay> <overlay>
<template id="infolog.edit.description" template="" lang="" group="0" version="1.6.001"> <template id="infolog.edit.description" template="" lang="" group="0" version="1.6.001">
<textbox multiline="true" id="info_des" no_lang="1" width="99.7%" height="245px"/> <et2-textarea id="info_des" no_lang="1"/>
<checkbox id="clean_history"/> <checkbox id="clean_history"/>
</template> </template>
<template id="infolog.edit.links" template="" lang="" group="0" version="1.3.001"> <template id="infolog.edit.links" template="" lang="" group="0" version="1.3.001">
@ -154,7 +154,7 @@
<rows> <rows>
<row class="dialogHeader"> <row class="dialogHeader">
<description value="Title" for="info_subject"/> <description value="Title" for="info_subject"/>
<textbox statustext="a short subject for the entry" id="info_subject" class="et2_fullWidth et2_required" maxlength="255" span="4" tabindex="1"/> <et2-textbox statustext="a short subject for the entry" id="info_subject" class="et2_fullWidth et2_required" maxlength="255" span="4" tabindex="1"></et2-textbox>
<textbox type="integer" id="info_number" readonly="true"/> <textbox type="integer" id="info_number" readonly="true"/>
<appicon src="infolog" for="info_number"/> <appicon src="infolog" for="info_number"/>
</row> </row>
@ -165,7 +165,9 @@
</menulist> </menulist>
<description/> <description/>
<description value="Startdate" for="info_startdate"/> <description value="Startdate" for="info_startdate"/>
<date-time statustext="when should the ToDo or Phonecall be started, it shows up from that date in the filter open or own open (startpage)" id="info_startdate" class="et2_fullWidth"/> <et2-datetime
statustext="when should the ToDo or Phonecall be started, it shows up from that date in the filter open or own open (startpage)"
id="info_startdate" class="et2_fullWidth"></et2-datetime>
</row> </row>
<row class="dialogHeader3"> <row class="dialogHeader3">
<description value="Contact"/> <description value="Contact"/>
@ -217,33 +219,34 @@
</row> </row>
<row disabled="!@info_owner" class="dialogOperators"> <row disabled="!@info_owner" class="dialogOperators">
<description value="Owner"/> <description value="Owner"/>
<hbox width="100%"> <et2-hbox>
<menulist> <select-account id="info_owner" readonly="true"/>
<menupopup type="select-account" id="info_owner" readonly="true"/>
</menulist>
<date-time id="info_created" readonly="true" align="right"/> <date-time id="info_created" readonly="true" align="right"/>
</hbox> </et2-hbox>
<description/> <description/>
<description value="Last modified"/> <description value="Last modified"/>
<hbox width="100%"> <et2-hbox>
<menulist> <select-account id="info_modifier" readonly="true"/>
<menupopup type="select-account" id="info_modifier" readonly="true"/>
</menulist>
<date-time id="info_datemodified" readonly="true" align="right"/> <date-time id="info_datemodified" readonly="true" align="right"/>
</hbox> </et2-hbox>
</row> </row>
<row class="dialogFooterToolbar"> <row class="dialogFooterToolbar">
<hbox span="6"> <hbox span="all">
<button statustext="Saves this entry" label="Save" id="button[save]" image="save" background_image="1"/> <button statustext="Saves this entry" label="Save" id="button[save]" image="save"
<button statustext="Apply the changes" label="Apply" id="button[apply]" image="apply" background_image="1"/> background_image="1"/>
<button statustext="leave without saveing the entry" label="Cancel" id="button[cancel]" onclick="window.close();" image="cancel" background_image="1"/> <button statustext="Apply the changes" label="Apply" id="button[apply]" image="apply"
<menulist> background_image="1"></button>
<menupopup statustext="Execute a further action for this entry" id="action" onchange="app.infolog.edit_actions()" options="Actions..."/> <button statustext="leave without saveing the entry" label="Cancel" id="button[cancel]"
</menulist> onclick="window.close();" image="cancel" background_image="1"/>
<checkbox label="Do not notify" id="no_notifications" statustext="Do not notify of these changes"/> <select statustext="Execute a further action for this entry" id="action"
</hbox> onchange="app.infolog.edit_actions()" empty_label="Actions..."/>
<button align="right" statustext="delete this entry" label="Delete" id="button[delete]" onclick="if($cont[info_anz_subs]) return $cont[info_anz_subs]; et2_dialog.confirm(widget,'Delete this entry','Delete');" image="delete" background_image="1" span="all"/> <checkbox label="Do not notify" id="no_notifications"
</row> statustext="Do not notify of these changes"/>
<button align="right" statustext="delete this entry" label="Delete" id="button[delete]"
onclick="if($cont[info_anz_subs]) return $cont[info_anz_subs]; et2_dialog.confirm(widget,'Delete this entry','Delete');"
image="delete" background_image="1" span="all"/>
</hbox>
</row>
</rows> </rows>
</grid> </grid>
</template> </template>

View File

@ -222,21 +222,21 @@
</rows> </rows>
</grid> </grid>
<styles> <styles>
/** /**
* Add / remove link or category popup used for actions on multiple entries * Add / remove link or category popup used for actions on multiple entries
*/ */
div.action_popup[id] { .action_popup[id] {
position: fixed; position: fixed;
top: 200px; top: 200px;
left: 450px; left: 450px;
width: 76ex; width: 76ex;
z-index: 20000; z-index: 20000;
display: none; display: none;
border-collapse:collapse; border-collapse:collapse;
border-spacing:0px border-spacing:0px
} }
.action_popup-content { .action_popup-content {
display:block; display:block;
padding:2ex; padding:2ex;
color:#666666; color:#666666;

View File

@ -132,7 +132,7 @@ button.infologExtraButton {
button.infologExtraButton:hover { button.infologExtraButton:hover {
background-size: 16px; background-size: 16px;
} }
div.et2_hbox.tab_toolbar { .tab_toolbar {
position: relative; position: relative;
} }
button#infolog-edit_encrypt { button#infolog-edit_encrypt {

View File

@ -1,47 +0,0 @@
<!--
@license
Copyright IBM Corp. 2019
This source code is licensed under the Apache-2.0 license found in the
LICENSE file in the root directory of this source tree.
-->
<html>
<head>
<title>carbon-web-components example</title>
<meta charset="UTF-8" />
<style type="text/css">
body {
font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
}
#app {
width: 300px;
}
bx-dropdown,
bx-dropdown-item {
visibility: hidden;
}
bx-dropdown:defined,
bx-dropdown-item:defined {
visibility: inherit;
}
</style>
</head>
<body>
<h1>Hello World! 👋</h1>
<div id="app">
<bx-dropdown trigger-content="Select an item">
<bx-dropdown-item value="all">Option 1</bx-dropdown-item>
<bx-dropdown-item value="cloudFoundry">Option 2</bx-dropdown-item>
<bx-dropdown-item value="staging">Option 3</bx-dropdown-item>
<bx-dropdown-item value="dea">Option 4</bx-dropdown-item>
<bx-dropdown-item value="router">Option 5</bx-dropdown-item>
</bx-dropdown>
</div>
<script type="module" src="js/app.min.js"></script>
</body>
</html>

View File

@ -1,11 +0,0 @@
/**
* @license
*
* Copyright IBM Corp. 2020
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import 'carbon-web-components/es/components/dropdown/dropdown.js';
import 'carbon-web-components/es/components/dropdown/dropdown-item.js';

View File

@ -319,7 +319,7 @@ app.classes.mail = AppJS.extend(
{ {
textAreaWidget.tinymce.then(()=>{ textAreaWidget.tinymce.then(()=>{
that.compose_resizeHandler(); that.compose_resizeHandler();
jQuery(textAreaWidget.editor.iframeElement.contentWindow.document).on('dragenter', function(){ if (textAreaWidget.editor) jQuery(textAreaWidget.editor.iframeElement.contentWindow.document).on('dragenter', function(){
// anything to bind on tinymce iframe // anything to bind on tinymce iframe
}); });
}); });

View File

@ -637,7 +637,7 @@ div.mailDisplayHeaders div.mail_extraEmails.visible
box-shadow: 5px 5px 5px #aaa; box-shadow: 5px 5px 5px #aaa;
border: 1px solid gray; border: 1px solid gray;
} }
div.mailComposeBody { .mailComposeBody {
white-space: normal !important; white-space: normal !important;
} }
#mail-compose_mail_plaintext { #mail-compose_mail_plaintext {
@ -884,11 +884,11 @@ span#mail-compose_replyto_expander {
padding: 0; padding: 0;
} }
/*Make file uploads in compose dialog invisible*/ /*Make file uploads in compose dialog invisible*/
div.mail-compose_toolbar_assist div.mail-compose_fileselector, #mail-compose_selectFromVFSForCompose, div.mail-compose_toolbar_assist { .mail-compose_toolbar_assist div.mail-compose_fileselector, #mail-compose_selectFromVFSForCompose, .mail-compose_toolbar_assist {
display:none; display:none;
} }
/*Make file uploads in compose dialog invisible*/ /*Make file uploads in compose dialog invisible*/
div.mail-compose_toolbar_assist div.mail-compose_fileselector, #mail-compose_selectFromVFSForCompose, div.mail-compose_toolbar_assist { .mail-compose_toolbar_assist div.mail-compose_fileselector, #mail-compose_selectFromVFSForCompose, .mail-compose_toolbar_assist {
display:none; display:none;
} }

View File

@ -86,12 +86,12 @@
</grid> </grid>
</vbox> </vbox>
<vbox class="mailComposeBodySection" width="100%"> <vbox class="mailComposeBodySection" width="100%">
<hbox disabled="@is_plain" class="mailComposeBody mailComposeHtmlContainer"> <old-hbox disabled="@is_plain" class="mailComposeBody mailComposeHtmlContainer">
<htmlarea name="mail_htmltext" id="mail_htmltext" statusbar="false" menubar="false" toolbar="@html_toolbar" imageUpload="link_to" expand_toolbar="true" height="478px" width="100%" resize_ratio="0"/> <htmlarea name="mail_htmltext" id="mail_htmltext" statusbar="false" menubar="false" toolbar="@html_toolbar" imageUpload="link_to" expand_toolbar="true" height="478px" width="100%" resize_ratio="0"/>
</hbox> </old-hbox>
<hbox disabled="@is_html" class="mailComposeBody mailComposeTextContainer"> <old-hbox disabled="@is_html" class="mailComposeBody mailComposeTextContainer">
<textbox multiline="true" rows="40" cols="120" width="100%" span="all" no_lang="1" name="mail_plaintext" id="mail_plaintext" resize_ratio="0"/> <textbox multiline="true" rows="40" cols="120" width="100%" span="all" no_lang="1" name="mail_plaintext" id="mail_plaintext" resize_ratio="0"/>
</hbox> </old-hbox>
<vbox class="et2_file mailUploadSection" disabled="@no_griddata"> <vbox class="et2_file mailUploadSection" disabled="@no_griddata">
<hbox> <hbox>
<select id="filemode" label="Send files as" onchange="app.mail.check_sharing_filemode"/> <select id="filemode" label="Send files as" onchange="app.mail.check_sharing_filemode"/>

View File

@ -5,43 +5,43 @@
<template id="mail.index.splitter" height="100%" template="" lang="" group="0" version="1.9.001"> <template id="mail.index.splitter" height="100%" template="" lang="" group="0" version="1.9.001">
<split dock_side="bottomDock" id="mailSplitter"> <split dock_side="bottomDock" id="mailSplitter">
<nextmatch id="nm" onselect="app.mail.mail_preview" class="" template="mail.index.rows" header_left="mail.index.add" header_right="mail.index.header_right" disable_selection_advance="true"/> <nextmatch id="nm" onselect="app.mail.mail_preview" class="" template="mail.index.rows" header_left="mail.index.add" header_right="mail.index.header_right" disable_selection_advance="true"/>
<vbox id="mailPreview" width="100%"> <old-vbox id="mailPreview" width="100%">
<toolbar id="toolbar"/> <toolbar id="toolbar"/>
<box id="blank" disabled="false"> <old-box id="blank" disabled="false">
<image src="mail"/> <image src="mail"/>
<description value="Select an item to read"/> <description value="Select an item to read"/>
</box> </old-box>
<hbox width="100%" id="mailPreviewHeadersFrom" class="mailPreviewHeaders"> <old-hbox width="100%" id="mailPreviewHeadersFrom" class="mailPreviewHeaders">
<description value="From"/> <description value="From"/>
<hbox id="additionalFromAddress" class="mail_extraEmails"> <old-hbox id="additionalFromAddress" class="mail_extraEmails">
</hbox> </old-hbox>
<buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/> <buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/>
</hbox> </old-hbox>
<hbox id="mailPreviewHeadersSubject" class="mailPreviewHeaders"> <old-hbox id="mailPreviewHeadersSubject" class="mailPreviewHeaders">
<description value="Subject"/> <description value="Subject"/>
<description align="left" id="previewSubject" readonly="true" hover_action="app.mail.modifyMessageSubjectDialog" hover_action_title="Modify subject of this message"/> <description align="left" id="previewSubject" readonly="true" hover_action="app.mail.modifyMessageSubjectDialog" hover_action_title="Modify subject of this message"/>
</hbox> </old-hbox>
<hbox id="mailPreviewHeadersDate" class="mailPreviewHeaders"> <old-hbox id="mailPreviewHeadersDate" class="mailPreviewHeaders">
<description value="Date"/> <description value="Date"/>
<date-time align="left" id="previewDate" readonly="true"/> <date-time align="left" id="previewDate" readonly="true"/>
</hbox> </old-hbox>
<hbox width="100%" id="mailPreviewHeadersTo" class="mailPreviewHeaders"> <old-hbox width="100%" id="mailPreviewHeadersTo" class="mailPreviewHeaders">
<description value="To"/> <description value="To"/>
<hbox id="additionalToAddress" class="mail_extraEmails"> <old-hbox id="additionalToAddress" class="mail_extraEmails">
</hbox> </old-hbox>
<buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/> <buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/>
</hbox> </old-hbox>
<hbox id="mailPreviewHeadersCC" class="mailPreviewHeaders"> <old-hbox id="mailPreviewHeadersCC" class="mailPreviewHeaders">
<description value="CC"/> <description value="CC"/>
<hbox id="additionalCCAddress" class="mail_extraEmails"> <old-hbox id="additionalCCAddress" class="mail_extraEmails">
</hbox> </old-hbox>
<buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/> <buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/>
</hbox> </old-hbox>
<hbox class="mailPreviewHeaders smimeIcons"> <old-hbox class="mailPreviewHeaders smimeIcons">
<image id="smime_signature" src="smime_sign" statustext="Smime signed message" disabled="true" align="right" width="24"/> <image id="smime_signature" src="smime_sign" statustext="Smime signed message" disabled="true" align="right" width="24"/>
<image id="smime_encryption" src="smime_encrypt" statustext="Smime encrypted message" disabled="true" align="right" width="24"/> <image id="smime_encryption" src="smime_encrypt" statustext="Smime encrypted message" disabled="true" align="right" width="24"/>
</hbox> </old-hbox>
<hbox id="mailPreviewHeadersAttachments" class="mailPreviewHeaders"> <old-hbox id="mailPreviewHeadersAttachments" class="mailPreviewHeaders">
<description value="Attachments"/> <description value="Attachments"/>
<grid disabled="@no_griddata" id="previewAttachmentArea" class="previewAttachmentArea egwGridView_grid"> <grid disabled="@no_griddata" id="previewAttachmentArea" class="previewAttachmentArea egwGridView_grid">
<columns> <columns>
@ -72,11 +72,11 @@
</rows> </rows>
</grid> </grid>
<buttononly class="et2_button ui-button" label="Show all attachments" image="foldertree_nolines_plus" width="16px" height="16px" onclick="app.mail.showAllHeader"/> <buttononly class="et2_button ui-button" label="Show all attachments" image="foldertree_nolines_plus" width="16px" height="16px" onclick="app.mail.showAllHeader"/>
</hbox> </old-hbox>
<box id="mailPreviewContainer"> <old-box id="mailPreviewContainer">
<iframe frameborder="1" id="messageIFRAME" scrolling="auto"/> <iframe frameborder="1" id="messageIFRAME" scrolling="auto"/>
</box> </old-box>
</vbox> </old-vbox>
</split> </split>
</template> </template>
<template id="mail.index.nosplitter" template="" lang="" group="0" version="1.9.001"> <template id="mail.index.nosplitter" template="" lang="" group="0" version="1.9.001">

View File

@ -850,15 +850,15 @@ span#mail-compose_replyto_expander {
padding: 0; padding: 0;
} }
/*Make file uploads in compose dialog invisible*/ /*Make file uploads in compose dialog invisible*/
div.mail-compose_toolbar_assist div.mail-compose_fileselector, .mail-compose_toolbar_assist div.mail-compose_fileselector,
#mail-compose_selectFromVFSForCompose, #mail-compose_selectFromVFSForCompose,
div.mail-compose_toolbar_assist { .mail-compose_toolbar_assist {
display: none; display: none;
} }
/*Make file uploads in compose dialog invisible*/ /*Make file uploads in compose dialog invisible*/
div.mail-compose_toolbar_assist div.mail-compose_fileselector, .mail-compose_toolbar_assist div.mail-compose_fileselector,
#mail-compose_selectFromVFSForCompose, #mail-compose_selectFromVFSForCompose,
div.mail-compose_toolbar_assist { .mail-compose_toolbar_assist {
display: none; display: none;
} }
div.mailComposeHeaderSection > table { div.mailComposeHeaderSection > table {
@ -2072,6 +2072,7 @@ table#mail-index_previewAttachmentArea::-webkit-scrollbar-thumb:hover {
#mail-index_nm.et2_nextmatch .egwGridView_outer thead tr { #mail-index_nm.et2_nextmatch .egwGridView_outer thead tr {
border-left: 12px solid #B4B4B4; border-left: 12px solid #B4B4B4;
} }
#mail-index_nm.et2_nextmatch.et2_nextmatch .egwGridView_outer .egwGridView_scrollarea tbody tr.row_category td:first-child > div { #mail-index_nm.et2_nextmatch .egwGridView_outer thead tr.row_category td:first-child > div {
margin-left: 0px; margin-left: 0px;
padding-left: 0px;
} }

8603
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,14 +6,23 @@
"repository": {}, "repository": {},
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",
"build:watch": "rollup -cw" "build:watch": "rollup -cw",
"jstest": "tsc &> /dev/null; web-test-runner",
"jstest:watch": "web-test-runner --watch"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.6", "@babel/core": "^7.14.6",
"@babel/preset-typescript": "^7.14.5", "@babel/preset-typescript": "^7.14.5",
"@open-wc/testing": "^2.5.33",
"@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-node-resolve": "^13.0.0", "@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1", "@rollup/plugin-typescript": "^8.2.1",
"@types/chai": "^4.2.21",
"@types/mocha": "^8.2.3",
"@web/dev-server-esbuild": "^0.2.14",
"@web/dev-server-rollup": "^0.3.9",
"@web/test-runner": "^0.13.16",
"@web/test-runner-playwright": "^0.8.8",
"grunt": "^1.3.0", "grunt": "^1.3.0",
"grunt-contrib-cssmin": "^2.2.1", "grunt-contrib-cssmin": "^2.2.1",
"grunt-newer": "^1.3.0", "grunt-newer": "^1.3.0",
@ -21,6 +30,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.52.2", "rollup": "^2.52.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"sinon": "^11.1.2",
"terser": "^4.8.0", "terser": "^4.8.0",
"typescript": "^3.9.7" "typescript": "^3.9.7"
}, },
@ -44,9 +54,13 @@
}, },
"dependencies": { "dependencies": {
"@andxor/jquery-ui-touch-punch-fix": "^1.0.2", "@andxor/jquery-ui-touch-punch-fix": "^1.0.2",
"@lion/button": "^0.14.2",
"@lion/core": "^0.18.2",
"@lion/input": "^0.15.4",
"@lion/input-date": "^0.12.6",
"@lion/input-datepicker": "^0.23.6",
"@lion/textarea": "^0.13.4",
"jquery-ui-timepicker-addon": "^1.6.3", "jquery-ui-timepicker-addon": "^1.6.3",
"lit-element": "^2.5.1",
"lit-html": "^1.4.1",
"sortablejs": "^1.14.0" "sortablejs": "^1.14.0"
}, },
"engines": { "engines": {

View File

@ -28,7 +28,19 @@ readdirSync('./chunks').forEach(name => {
}); });
// Turn on minification // Turn on minification
const do_minify = true; const do_minify = false;
function isBareSpecifier (id) {
if (id.startsWith("./") || id.startsWith("../") || id.startsWith("/"))
return false;
try {
new URL(id);
return false;
}
catch {
return true;
}
}
const config = { const config = {
treeshake: false, treeshake: false,
@ -65,13 +77,18 @@ const config = {
}, },
plugins: [{ plugins: [{
resolveId (id, parentId) { resolveId (id, parentId) {
// Delegate bare specifiers to node_modules resolver
if (isBareSpecifier(id))
{
return;
}
if (!parentId || parentId.indexOf(path.sep + 'node_modules' + path.sep) !== -1) if (!parentId || parentId.indexOf(path.sep + 'node_modules' + path.sep) !== -1)
{ {
return; return;
} }
if(id.endsWith(".js")) if (id.endsWith(".js"))
{ {
const tsPath =path.resolve(path.dirname(parentId), id.slice(0,-3) + '.ts'); const tsPath = path.resolve(path.dirname(parentId), id.slice(0,-3) + '.ts');
try { try {
readFileSync(tsPath); readFileSync(tsPath);
console.warn(id + " is a TS file loaded with wrong extension. Remove the extension on the import in " + parentId); console.warn(id + " is a TS file loaded with wrong extension. Remove the extension on the import in " + parentId);
@ -93,7 +110,9 @@ const config = {
} }
}, },
// resolve (external) node modules from node_modules directory // resolve (external) node modules from node_modules directory
resolve(), resolve({
browser: true
}),
{ {
transform (code, id) { transform (code, id) {
if (id.endsWith('.ts')) if (id.endsWith('.ts'))

View File

@ -0,0 +1,75 @@
/**
* This is the configuration file for automatic TypeScript testing
*
* It uses "web-test-runner" to run the tests, which are written using
* Mocha (https://mochajs.org/) & Chai Assertion Library (https://www.chaijs.com/api/assert/)
* Playwright (https://playwright.dev/docs/intro) runs the tests in actual browsers.
*
* Trouble getting tests to run? Try manually compiling TypeScript (source & tests), that seems to help.
*/
import fs from 'fs';
import {playwrightLauncher} from '@web/test-runner-playwright';
import {esbuildPlugin} from '@web/dev-server-esbuild';
// Get tests for web components (in their own directory)
const webComponents = fs.readdirSync('api/js/etemplate')
.filter(
dir => fs.statSync(`api/js/etemplate/${dir}`).isDirectory() && fs.existsSync(`api/js/etemplate/${dir}/test`),
)
.map(dir => `api/js/etemplate/${dir}/test`);
// Add any test files in app/js/test/
const appJS = fs.readdirSync('.')
.filter(
dir => fs.existsSync(`${dir}/js`) && fs.existsSync(`${dir}/js/test`) && fs.statSync(`${dir}/js/test`).isDirectory(),
)
export default {
nodeResolve: true,
coverageConfig: {
report: true,
reportDir: 'coverage',
threshold: {
statements: 90,
branches: 65,
functions: 80,
lines: 90,
},
},
testFramework: {
config: {
timeout: '3000',
},
},
browsers: [
playwrightLauncher({product: 'firefox', concurrency: 1}),
playwrightLauncher({product: 'chromium'}),
// Dependant on specific versions of shared libraries (libicuuc.so.66, latest is .67)
//playwrightLauncher({ product: 'webkit' }),
],
groups:
webComponents.map(pkg =>
{
return {
name: `${pkg}`,
files: `${pkg}/*.test.ts`
};
}).concat(
appJS.map(app =>
{
return {
name: app,
files: `${app}/js/**/*.test.ts`
}
}
)
)
,
plugins: [
// Handles typescript
esbuildPlugin({ts: true})
],
};