mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 23:00:56 +01:00
Merge web-components branch
This commit is contained in:
commit
e3ca8b8f7f
@ -83,9 +83,7 @@
|
||||
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_fax,&hearts;"/>
|
||||
<description for="adr_one_countryname" value="country"/>
|
||||
<vbox class="city_state_postcode" width="100%">
|
||||
<menulist class="et2_fullWidth">
|
||||
<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>
|
||||
<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"/>
|
||||
<textbox id="adr_one_countryname" class="custom_country et2_fullWidth" autocomplete="country-name"/>
|
||||
</vbox>
|
||||
<description/>
|
||||
@ -116,9 +114,9 @@
|
||||
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_fax,&hearts;"/>
|
||||
<description for="adr_one_countryname" value="country"/>
|
||||
<vbox width="100%" tabindex="16">
|
||||
<menulist tabindex="16">
|
||||
<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"/>
|
||||
</menulist>
|
||||
<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"/>
|
||||
<textbox id="adr_one_countryname" class="custom_country et2_fullWidth" tabindex="16" autocomplete="country-name"/>
|
||||
</vbox>
|
||||
<description/>
|
||||
@ -137,9 +135,7 @@
|
||||
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_car,&hearts;"/>
|
||||
<description disabled="@no_tid" for="tid" value="Type"/>
|
||||
<hbox width="102%">
|
||||
<menulist disabled="@no_tid">
|
||||
<menupopup id="tid" no_lang="1" class="et2_fullWidth" onchange="1"/>
|
||||
</menulist>
|
||||
<select id="tid" no_lang="1" class="et2_fullWidth" onchange="1" disabled="@no_tid"/>
|
||||
</hbox>
|
||||
<description/>
|
||||
</row>
|
||||
@ -247,9 +243,9 @@
|
||||
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_pager,&hearts;"/>
|
||||
<description for="adr_two_countryname" value="country"/>
|
||||
<vbox width="100%" class="city_state_postcode">
|
||||
<menulist class="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" />
|
||||
</menulist>
|
||||
<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"/>
|
||||
<textbox id="adr_two_countryname" class="custom_country et2_fullWidth" autocomplete="section-two country-name" />
|
||||
</vbox>
|
||||
<description/>
|
||||
@ -280,9 +276,9 @@
|
||||
<radio statustext="select phone number as prefered way of contact" id="tel_prefer" options="tel_pager,&hearts;"/>
|
||||
<description for="adr_two_countryname" value="country"/>
|
||||
<vbox width="100%">
|
||||
<menulist>
|
||||
<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" />
|
||||
</menulist>
|
||||
<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"/>
|
||||
<textbox id="adr_two_countryname" class="custom_country et2_fullWidth" autocomplete="section-two country-name" />
|
||||
</vbox>
|
||||
<description/>
|
||||
@ -416,9 +412,9 @@
|
||||
</row>
|
||||
<row class="dialogOperators">
|
||||
<description value="Addressbook"/>
|
||||
<menulist class="et2_fullWidth">
|
||||
<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;" />
|
||||
</menulist>
|
||||
<select class="owner et2_fullWidth" statustext="Addressbook the contact should be saved to"
|
||||
id="owner" no_lang="1"
|
||||
onchange="widget.getInstanceManager().submit(null,false,true); return false;"/>
|
||||
<description/>
|
||||
<description value="own sorting"/>
|
||||
<menulist>
|
||||
|
@ -232,7 +232,6 @@ select#addressbook-index_col_filter\[tid\] {
|
||||
#addressbook-edit {
|
||||
height: auto;
|
||||
min-height: 390px;
|
||||
overflow: auto;
|
||||
}
|
||||
/*##################################################################*/
|
||||
/*Infolog*/
|
||||
|
@ -84,7 +84,6 @@
|
||||
#addressbook-edit {
|
||||
height: auto;
|
||||
min-height: 390px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/*##################################################################*/
|
||||
|
@ -175,15 +175,15 @@ class AdminApp extends EgwApp
|
||||
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
|
||||
// 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);
|
||||
if(old) old.clear();
|
||||
});
|
||||
jQuery(this.ajax_target.node).empty();
|
||||
jQuery(this.ajax_target.getDOMNode()).empty();
|
||||
}
|
||||
this.egw.json(
|
||||
framework.activeApp.getMenuaction('ajax_exec', _url),
|
||||
@ -394,7 +394,7 @@ class AdminApp extends EgwApp
|
||||
if(!_data || _data.type != undefined) return;
|
||||
|
||||
// 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
126
api/etemplate.php
Normal 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
|
||||
}
|
130
api/js/etemplate/Et2Box/Et2Box.ts
Normal file
130
api/js/etemplate/Et2Box/Et2Box.ts
Normal 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);
|
32
api/js/etemplate/Et2Box/test/Et2Box.test.ts
Normal file
32
api/js/etemplate/Et2Box/test/Et2Box.test.ts
Normal 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'));
|
||||
})
|
||||
});
|
292
api/js/etemplate/Et2Button/Et2Button.ts
Normal file
292
api/js/etemplate/Et2Button/Et2Button.ts
Normal 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);
|
69
api/js/etemplate/Et2Button/test/Et2Button.test.ts
Normal file
69
api/js/etemplate/Et2Button/test/Et2Button.test.ts
Normal 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"));
|
||||
})
|
||||
});
|
151
api/js/etemplate/Et2Colorpicker/Et2Colorpicker.ts
Normal file
151
api/js/etemplate/Et2Colorpicker/Et2Colorpicker.ts
Normal 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);
|
38
api/js/etemplate/Et2Colorpicker/test/Et2Colorpicker.test.ts
Normal file
38
api/js/etemplate/Et2Colorpicker/test/Et2Colorpicker.test.ts
Normal 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(), "");
|
||||
});
|
||||
|
||||
});
|
441
api/js/etemplate/Et2Date/Et2Date.ts
Normal file
441
api/js/etemplate/Et2Date/Et2Date.ts
Normal 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);
|
69
api/js/etemplate/Et2Date/Et2DateTime.ts
Normal file
69
api/js/etemplate/Et2Date/Et2DateTime.ts
Normal 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);
|
125
api/js/etemplate/Et2Date/test/Et2Date.test.ts
Normal file
125
api/js/etemplate/Et2Date/test/Et2Date.test.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
});
|
125
api/js/etemplate/Et2Date/test/Et2DateTime.test.ts
Normal file
125
api/js/etemplate/Et2Date/test/Et2DateTime.test.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
});
|
100
api/js/etemplate/Et2Date/test/Formatter.test.ts
Normal file
100
api/js/etemplate/Et2Date/test/Formatter.test.ts
Normal 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);
|
||||
|
||||
}
|
||||
});
|
||||
});
|
150
api/js/etemplate/Et2Date/test/Parser.test.ts
Normal file
150
api/js/etemplate/Et2Date/test/Parser.test.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
164
api/js/etemplate/Et2Iframe/Et2Iframe.ts
Normal file
164
api/js/etemplate/Et2Iframe/Et2Iframe.ts
Normal 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);
|
156
api/js/etemplate/Et2InputWidget/Et2InputWidget.ts
Normal file
156
api/js/etemplate/Et2InputWidget/Et2InputWidget.ts
Normal 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);
|
39
api/js/etemplate/Et2InputWidget/test/InputBase.test.ts
Normal file
39
api/js/etemplate/Et2InputWidget/test/InputBase.test.ts
Normal 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>
|
||||
*/
|
128
api/js/etemplate/Et2Textarea/Et2Textarea.ts
Normal file
128
api/js/etemplate/Et2Textarea/Et2Textarea.ts
Normal 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);
|
49
api/js/etemplate/Et2Textbox/Et2Textbox.ts
Normal file
49
api/js/etemplate/Et2Textbox/Et2Textbox.ts
Normal 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);
|
32
api/js/etemplate/Et2Textbox/test/Et2Textbox.test.ts
Normal file
32
api/js/etemplate/Et2Textbox/test/Et2Textbox.test.ts
Normal 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'));
|
||||
})
|
||||
});
|
34
api/js/etemplate/Et2Textbox/test/Et2TextboxValues.test.ts
Normal file
34
api/js/etemplate/Et2Textbox/test/Et2TextboxValues.test.ts
Normal 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))
|
||||
|
||||
*/
|
||||
})
|
||||
});
|
1130
api/js/etemplate/Et2Widget/Et2Widget.ts
Normal file
1130
api/js/etemplate/Et2Widget/Et2Widget.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
||||
*/
|
@ -238,7 +238,7 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
|
||||
|
||||
// Append this node at its index
|
||||
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);
|
||||
}
|
||||
@ -283,9 +283,19 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
|
||||
}
|
||||
}
|
||||
// _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 : "") + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,73 +12,10 @@
|
||||
et2_core_common;
|
||||
*/
|
||||
|
||||
import {egw} from "../jsapi/egw_global";
|
||||
import {et2_checkType, et2_no_init, et2_validateAttrib} from "./et2_core_common";
|
||||
import {et2_implements_registry} from "./et2_core_interfaces";
|
||||
import {egw, IegwAppLocal} from "../jsapi/egw_global";
|
||||
import {et2_checkType, et2_cloneObject, et2_no_init, et2_validateAttrib} from "./et2_core_common";
|
||||
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
|
||||
{
|
||||
/**
|
||||
@ -91,7 +28,7 @@ export class ClassWithInterfaces
|
||||
*
|
||||
* @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))
|
||||
@ -106,7 +43,7 @@ export class ClassWithInterfaces
|
||||
*
|
||||
* @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')
|
||||
{
|
||||
@ -133,13 +70,19 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
getAttribute(_name)
|
||||
{
|
||||
if (typeof this.attributes[_name] != "undefined" &&
|
||||
!this.attributes[_name].ignore) {
|
||||
if (typeof this["get_" + _name] == "function") {
|
||||
!this.attributes[_name].ignore)
|
||||
{
|
||||
if (typeof this["get_" + _name] == "function")
|
||||
{
|
||||
return this["get_" + _name]();
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return this[_name];
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
egw.debug("error", this, "Attribute '" + _name + "' does not exist!");
|
||||
}
|
||||
}
|
||||
@ -156,22 +99,30 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
*/
|
||||
setAttribute(_name, _value, _override)
|
||||
{
|
||||
if (typeof this.attributes[_name] != "undefined") {
|
||||
if (!this.attributes[_name].ignore) {
|
||||
if (typeof _override == "undefined") {
|
||||
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") {
|
||||
if (typeof this["set_" + _name] == "function")
|
||||
{
|
||||
this["set_" + _name](val);
|
||||
} else if (_override || typeof this[_name] == "undefined") {
|
||||
}
|
||||
else if (_override || typeof this[_name] == "undefined")
|
||||
{
|
||||
this[_name] = val;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
egw.debug("warn", this, "Attribute '" + _name + "' does not exist!");
|
||||
}
|
||||
}
|
||||
@ -186,13 +137,18 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
static generateAttributeSet(widget, _attrs)
|
||||
{
|
||||
// Sanity check and validation
|
||||
for (var key in _attrs) {
|
||||
if (typeof widget[key] != "undefined") {
|
||||
if (!widget[key].ignore) {
|
||||
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 {
|
||||
}
|
||||
else
|
||||
{
|
||||
// Key does not exist - delete it and issue a warning
|
||||
delete (_attrs[key]);
|
||||
egw.debug("warn", this, "Attribute '" + key +
|
||||
@ -201,10 +157,13 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
}
|
||||
|
||||
// Include default values or already set values for this attribute
|
||||
for (var key in widget) {
|
||||
if (typeof _attrs[key] == "undefined") {
|
||||
for (var key in widget)
|
||||
{
|
||||
if (typeof _attrs[key] == "undefined")
|
||||
{
|
||||
var _default = widget[key]["default"];
|
||||
if (_default == et2_no_init) {
|
||||
if (_default == et2_no_init)
|
||||
{
|
||||
_default = undefined;
|
||||
}
|
||||
|
||||
@ -225,8 +184,10 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
*/
|
||||
initAttributes(_attrs)
|
||||
{
|
||||
for (var key in _attrs) {
|
||||
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) {
|
||||
for (var key in _attrs)
|
||||
{
|
||||
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined))
|
||||
{
|
||||
this.setAttribute(key, _attrs[key], false);
|
||||
}
|
||||
}
|
||||
@ -237,13 +198,16 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
let class_tree = [];
|
||||
let attributes = {};
|
||||
let n = 0;
|
||||
do {
|
||||
do
|
||||
{
|
||||
n++;
|
||||
class_tree.push(class_prototype);
|
||||
class_prototype = Object.getPrototypeOf(class_prototype);
|
||||
} while (class_prototype !== ClassWithAttributes && n < 50);
|
||||
}
|
||||
while (class_prototype !== ClassWithAttributes && n < 50);
|
||||
|
||||
for (let i = class_tree.length - 1; i >= 0; i--) {
|
||||
for (let i = class_tree.length - 1; i >= 0; i--)
|
||||
{
|
||||
attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes);
|
||||
}
|
||||
return attributes;
|
||||
@ -264,15 +228,19 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
var result = {};
|
||||
|
||||
// Copy the new object
|
||||
if (typeof _new != "undefined") {
|
||||
for (var key in _new) {
|
||||
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") {
|
||||
for (var key in _old)
|
||||
{
|
||||
if (typeof result[key] == "undefined")
|
||||
{
|
||||
result[key] = _old[key];
|
||||
}
|
||||
}
|
||||
@ -283,23 +251,27 @@ export class ClassWithAttributes extends ClassWithInterfaces
|
||||
var attributes = {};
|
||||
|
||||
// Copy the old attributes
|
||||
for (var key in _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) {
|
||||
for (var key in _parent)
|
||||
{
|
||||
var _old = _parent[key];
|
||||
|
||||
attributes[key] = _copyMerge(attributes[key], _old);
|
||||
}
|
||||
|
||||
// Validate the attributes
|
||||
for (var key in attributes) {
|
||||
for (var key in attributes)
|
||||
{
|
||||
et2_validateAttrib(key, attributes[key]);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,17 +15,19 @@
|
||||
*/
|
||||
|
||||
import {et2_no_init} from "./et2_core_common";
|
||||
import { ClassWithAttributes } from "./et2_core_inheritance";
|
||||
import { et2_widget, WidgetConfig } from "./et2_core_widget";
|
||||
import { et2_valueWidget } from './et2_core_valueWidget'
|
||||
import {et2_IInput, et2_ISubmitListener} from "./et2_core_interfaces";
|
||||
import {ClassWithAttributes} from "./et2_core_inheritance";
|
||||
import {et2_widget, WidgetConfig} from "./et2_core_widget";
|
||||
import {et2_valueWidget} from './et2_core_valueWidget'
|
||||
import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "./et2_core_interfaces";
|
||||
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
|
||||
// fixing circular dependencies by only importing the type (not in compiled .js)
|
||||
import type {et2_tabbox} from "./et2_widget_tabs";
|
||||
|
||||
export interface et2_input {
|
||||
getInputNode() : HTMLInputElement|HTMLElement;
|
||||
export interface et2_input
|
||||
{
|
||||
getInputNode(): HTMLInputElement | HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* et2_inputWidget derrives from et2_simpleWidget and implements the IInput
|
||||
* interface. When derriving from this class, call setDOMNode with an input
|
||||
@ -33,7 +35,7 @@ export interface 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": {
|
||||
"name": "Required",
|
||||
"default": false,
|
||||
@ -78,7 +80,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
|
||||
constructor(_parent, _attrs?: WidgetConfig, _child?: object)
|
||||
{
|
||||
// Call the inherited constructor
|
||||
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
|
||||
*/
|
||||
doLoadingFinished() : boolean | JQueryPromise<unknown>
|
||||
doLoadingFinished(): boolean | JQueryPromise<unknown>
|
||||
{
|
||||
let result = super.doLoadingFinished();
|
||||
|
||||
@ -141,10 +143,12 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
{
|
||||
jQuery(node)
|
||||
.off('.et2_inputWidget')
|
||||
.bind("change.et2_inputWidget", this, function(e) {
|
||||
.bind("change.et2_inputWidget", this, function (e)
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
@ -173,14 +177,16 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
|
||||
if (valid && this.onchange)
|
||||
{
|
||||
if(typeof this.onchange == 'function')
|
||||
if (typeof this.onchange == 'function')
|
||||
{
|
||||
// Make sure function gets a reference to the widget
|
||||
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);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
if(typeof this.options.onfocus == 'function')
|
||||
if (typeof this.options.onfocus == 'function')
|
||||
{
|
||||
// Make sure function gets a reference to the widget
|
||||
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);
|
||||
}
|
||||
@ -206,13 +212,13 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
*
|
||||
* @param {string} _value value to set
|
||||
*/
|
||||
set_value(_value : any | null)
|
||||
set_value(_value: any | null)
|
||||
{
|
||||
var node = this.getInputNode();
|
||||
if (node)
|
||||
{
|
||||
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();
|
||||
}
|
||||
@ -223,7 +229,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
set_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
|
||||
// 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();
|
||||
if (node)
|
||||
{
|
||||
if(_value && !this.options.readonly) {
|
||||
if (_value && !this.options.readonly)
|
||||
{
|
||||
jQuery(node).attr("required", "required");
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
node.removeAttribute("required");
|
||||
}
|
||||
}
|
||||
@ -273,8 +282,8 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
jQuery(node).addClass("invalid");
|
||||
|
||||
// If on a tab, switch to that tab so user can see it
|
||||
let widget : et2_widget = this;
|
||||
while(widget.getParent() && widget.getType() != 'tabbox')
|
||||
let widget: et2_widget = this;
|
||||
while (widget.getParent() && widget.getType() != 'tabbox')
|
||||
{
|
||||
widget = widget.getParent();
|
||||
}
|
||||
@ -319,26 +328,26 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
isDirty()
|
||||
{
|
||||
let value = this.getValue();
|
||||
if(typeof value !== typeof this._oldValue)
|
||||
if (typeof value !== typeof this._oldValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(this._oldValue === value)
|
||||
if (this._oldValue === value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
switch(typeof this._oldValue)
|
||||
switch (typeof this._oldValue)
|
||||
{
|
||||
case "object":
|
||||
if(typeof this._oldValue.length !== "undefined" &&
|
||||
if (typeof this._oldValue.length !== "undefined" &&
|
||||
this._oldValue.length !== value.length
|
||||
)
|
||||
{
|
||||
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;
|
||||
default:
|
||||
@ -382,4 +391,3 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,8 @@ export function et2_compileLegacyJS(_code, _widget, _context)
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,10 +22,10 @@ import {egw, IegwAppLocal} from "../jsapi/egw_global";
|
||||
import {et2_cloneObject, et2_csvSplit} from "./et2_core_common";
|
||||
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
|
||||
import {et2_IDOMNode, et2_IInputNode} from "./et2_core_interfaces";
|
||||
import {loadWebComponent} from "./Et2Widget/Et2Widget";
|
||||
// fixing circular dependencies by only importing type
|
||||
import type {et2_container} from "./et2_core_baseWidget";
|
||||
import type {et2_inputWidget, et2_input} from "./et2_core_inputWidget";
|
||||
import {decorateLanguageService} from "ts-lit-plugin/lib/decorate-language-service";
|
||||
import type {et2_inputWidget} from "./et2_core_inputWidget";
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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";
|
||||
|
||||
@ -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)
|
||||
if (typeof window.et2_createWidget === 'undefined') window['et2_createWidget'] = et2_createWidget;
|
||||
|
||||
export interface WidgetConfig {
|
||||
export interface WidgetConfig
|
||||
{
|
||||
type?: string;
|
||||
readonly?: boolean;
|
||||
width?: number;
|
||||
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
@ -191,7 +193,7 @@ export class et2_widget extends ClassWithAttributes
|
||||
|
||||
private _type: string;
|
||||
id: string;
|
||||
supportedWidgetClasses : any[];
|
||||
supportedWidgetClasses: any[];
|
||||
options: WidgetConfig;
|
||||
readonly: boolean;
|
||||
|
||||
@ -208,21 +210,24 @@ export class et2_widget extends ClassWithAttributes
|
||||
* @param _attrs is an associative array of attributes.
|
||||
* @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
|
||||
this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {});
|
||||
|
||||
// Check whether all attributes are available
|
||||
if (typeof _parent == "undefined") {
|
||||
if (typeof _parent == "undefined")
|
||||
{
|
||||
_parent = null;
|
||||
}
|
||||
|
||||
if (typeof _attrs == "undefined") {
|
||||
if (typeof _attrs == "undefined")
|
||||
{
|
||||
_attrs = {};
|
||||
}
|
||||
|
||||
if (_attrs.attributes) {
|
||||
if (_attrs.attributes)
|
||||
{
|
||||
jQuery.extend(_attrs, _attrs.attributes);
|
||||
}
|
||||
// Initialize all important parameters
|
||||
@ -233,22 +238,26 @@ export class et2_widget extends ClassWithAttributes
|
||||
this.id = _attrs["id"];
|
||||
|
||||
// Add this widget to the given parent widget
|
||||
if (_parent != null) {
|
||||
if (_parent != null)
|
||||
{
|
||||
_parent.addChild(this);
|
||||
}
|
||||
|
||||
// The supported widget classes array defines a whitelist for all widget
|
||||
// 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
|
||||
if (this._createNamespace()) {
|
||||
if (this._createNamespace())
|
||||
{
|
||||
this.checkCreateNamespace(_attrs);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.id) {
|
||||
if (this.id)
|
||||
{
|
||||
//this.id = this.id.replace(/\[/g,'[').replace(/]/g,']');
|
||||
}
|
||||
|
||||
@ -271,29 +280,33 @@ export class et2_widget extends ClassWithAttributes
|
||||
destroy()
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Free the array managers if they belong to this widget
|
||||
for (var key in this._mgrs) {
|
||||
if (this._mgrs[key] && this._mgrs[key].owner == this) {
|
||||
for (var key in this._mgrs)
|
||||
{
|
||||
if (this._mgrs[key] && this._mgrs[key].owner == this)
|
||||
{
|
||||
this._mgrs[key].destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getType() : string
|
||||
getType(): string
|
||||
{
|
||||
return this._type;
|
||||
}
|
||||
|
||||
setType(_type : string)
|
||||
setType(_type: string)
|
||||
{
|
||||
this._type = _type;
|
||||
}
|
||||
@ -308,7 +321,8 @@ export class et2_widget extends ClassWithAttributes
|
||||
clone(_parent)
|
||||
{
|
||||
// Default _parent to null
|
||||
if (typeof _parent == "undefined") {
|
||||
if (typeof _parent == "undefined")
|
||||
{
|
||||
_parent = null;
|
||||
}
|
||||
|
||||
@ -323,12 +337,14 @@ export class et2_widget extends ClassWithAttributes
|
||||
|
||||
assign(_obj)
|
||||
{
|
||||
if (typeof _obj._children == "undefined") {
|
||||
if (typeof _obj._children == "undefined")
|
||||
{
|
||||
this.egw().debug("log", "Foo!");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -341,7 +357,7 @@ export class et2_widget extends ClassWithAttributes
|
||||
/**
|
||||
* Returns the parent widget of this widget
|
||||
*/
|
||||
getParent() : et2_widget | null
|
||||
getParent(): et2_widget | null
|
||||
{
|
||||
return this._parent;
|
||||
}
|
||||
@ -351,7 +367,7 @@ export class et2_widget extends ClassWithAttributes
|
||||
/**
|
||||
* Returns the list of children of this widget.
|
||||
*/
|
||||
getChildren() : et2_widget[]
|
||||
getChildren(): et2_widget[]
|
||||
{
|
||||
return this._children;
|
||||
}
|
||||
@ -359,11 +375,14 @@ export class et2_widget extends ClassWithAttributes
|
||||
/**
|
||||
* Returns the base widget
|
||||
*/
|
||||
getRoot() : et2_container
|
||||
getRoot(): et2_container
|
||||
{
|
||||
if (this._parent != null)
|
||||
{
|
||||
if (this._parent != null) {
|
||||
return this._parent.getRoot();
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
* of et2_widget
|
||||
*/
|
||||
addChild(_node : et2_widget)
|
||||
addChild(_node: et2_widget)
|
||||
{
|
||||
this.insertChild(_node, this._children.length);
|
||||
}
|
||||
@ -386,18 +405,22 @@ export class et2_widget extends ClassWithAttributes
|
||||
* of et2_widget
|
||||
* @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.
|
||||
if (this.isOfSupportedWidgetClass(_node)) {
|
||||
if (this.isOfSupportedWidgetClass(_node))
|
||||
{
|
||||
// Remove the node from its original parent
|
||||
if (_node._parent) {
|
||||
if (_node._parent)
|
||||
{
|
||||
_node._parent.removeChild(_node);
|
||||
}
|
||||
|
||||
_node._parent = this;
|
||||
this._children.splice(_idx, 0, _node);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this);
|
||||
// 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
|
||||
var idx = this._children.indexOf(_node);
|
||||
|
||||
if (idx >= 0) {
|
||||
if (idx >= 0)
|
||||
{
|
||||
// This element is no longer parent of the child
|
||||
_node._parent = null;
|
||||
|
||||
@ -426,24 +450,29 @@ export class et2_widget extends ClassWithAttributes
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
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);
|
||||
|
||||
if (elem != null) {
|
||||
if (elem != null)
|
||||
{
|
||||
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 widget : et2_widget = this;
|
||||
for (var i = 0; i < ids.length && widget !== null; i++) {
|
||||
var widget: et2_widget = this;
|
||||
for (var i = 0; i < ids.length && widget !== null; i++)
|
||||
{
|
||||
widget = widget.getWidgetById(ids[i]);
|
||||
}
|
||||
return widget;
|
||||
@ -462,15 +491,18 @@ export class et2_widget extends ClassWithAttributes
|
||||
*/
|
||||
iterateOver(_callback, _context, _type?)
|
||||
{
|
||||
if (typeof _type == "undefined") {
|
||||
if (typeof _type == "undefined")
|
||||
{
|
||||
_type = et2_widget;
|
||||
}
|
||||
|
||||
if (this.isInTree() && this.instanceOf(_type)) {
|
||||
if (this.isInTree() && this.instanceOf(_type))
|
||||
{
|
||||
_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);
|
||||
}
|
||||
}
|
||||
@ -486,13 +518,15 @@ export class et2_widget extends ClassWithAttributes
|
||||
* return this._super(inTree);
|
||||
* 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;
|
||||
}
|
||||
|
||||
if (this._parent) {
|
||||
if (this._parent)
|
||||
{
|
||||
return _vis && this._parent.isInTree(this);
|
||||
}
|
||||
|
||||
@ -501,8 +535,10 @@ export class et2_widget extends ClassWithAttributes
|
||||
|
||||
isOfSupportedWidgetClass(_obj)
|
||||
{
|
||||
for (var i = 0; i < this.supportedWidgetClasses.length; i++) {
|
||||
if (_obj instanceof this.supportedWidgetClasses[i]) {
|
||||
for (var i = 0; i < this.supportedWidgetClasses.length; i++)
|
||||
{
|
||||
if (_obj instanceof this.supportedWidgetClasses[i])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -521,47 +557,55 @@ export class et2_widget extends ClassWithAttributes
|
||||
parseXMLAttrs(_attrsObj, _target, _proto)
|
||||
{
|
||||
// Check whether the attributes object is really existing, if not abort
|
||||
if (typeof _attrsObj == "undefined") {
|
||||
if (typeof _attrsObj == "undefined")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate over the given attributes and parse them
|
||||
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 attrValue = _attrsObj[i].value;
|
||||
|
||||
// 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 attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {};
|
||||
// Check for modifications on legacy options here. Normal modifications
|
||||
// are handled in widget constructor, but it's too late for legacy options then
|
||||
if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) {
|
||||
var mod : any = this.getArrayMgr("modifications").getEntry(_target.id);
|
||||
if (_target.id && 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;
|
||||
}
|
||||
// expand legacyOptions with content
|
||||
if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) {
|
||||
if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1)
|
||||
{
|
||||
attrValue = mgr.expandName(attrValue);
|
||||
}
|
||||
|
||||
// Parse the legacy options (as a string, other types not allowed)
|
||||
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
|
||||
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
|
||||
if (typeof _target[legacy[j]] === "undefined") {
|
||||
if (typeof _target[legacy[j]] === "undefined")
|
||||
{
|
||||
attrValue = splitted[j];
|
||||
|
||||
/**
|
||||
If more legacy options than expected, stuff them all in the last legacy option
|
||||
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);
|
||||
}
|
||||
|
||||
@ -569,26 +613,37 @@ export class et2_widget extends ClassWithAttributes
|
||||
|
||||
// If the attribute is marked as boolean, parse the
|
||||
// expression as bool expression.
|
||||
if (attr.type == "boolean") {
|
||||
if (attr.type == "boolean")
|
||||
{
|
||||
attrValue = mgr.parseBoolExpression(attrValue);
|
||||
} else if (typeof attrValue != "object") {
|
||||
}
|
||||
else if (typeof attrValue != "object")
|
||||
{
|
||||
attrValue = mgr.expandName(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
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
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];
|
||||
|
||||
// If the attribute is marked as boolean, parse the
|
||||
// expression as bool expression.
|
||||
if (attr.type == "boolean") {
|
||||
if (attr.type == "boolean")
|
||||
{
|
||||
attrValue = mgr.parseBoolExpression(attrValue);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
attrValue = mgr.expandName(attrValue);
|
||||
}
|
||||
}
|
||||
@ -609,20 +664,26 @@ export class et2_widget extends ClassWithAttributes
|
||||
{
|
||||
|
||||
// Apply the content of the modifications array
|
||||
if (this.id) {
|
||||
if (typeof this.id != "string") {
|
||||
if (this.id)
|
||||
{
|
||||
if (typeof this.id != "string")
|
||||
{
|
||||
console.log(this.id);
|
||||
}
|
||||
|
||||
if (this.getArrayMgr("modifications")) {
|
||||
if (this.getArrayMgr("modifications"))
|
||||
{
|
||||
var data = this.getArrayMgr("modifications").getEntry(this.id);
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
for (var key in data) {
|
||||
if (typeof data === 'object')
|
||||
{
|
||||
for (var key in data)
|
||||
{
|
||||
_attrs[key] = data[key];
|
||||
}
|
||||
}
|
||||
@ -630,16 +691,19 @@ export class et2_widget extends ClassWithAttributes
|
||||
}
|
||||
|
||||
// Translate the attributes
|
||||
for (var key in _attrs) {
|
||||
if (_attrs[key] && typeof this.attributes[key] != "undefined") {
|
||||
for (var key in _attrs)
|
||||
{
|
||||
if (_attrs[key] && typeof this.attributes[key] != "undefined")
|
||||
{
|
||||
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];
|
||||
// allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname}
|
||||
if (value.indexOf('{') !== -1)
|
||||
{
|
||||
const egw = this.egw();
|
||||
_attrs[key] = value.replace(/{([^}]+)}/g, function(str,p1)
|
||||
_attrs[key] = value.replace(/{([^}]+)}/g, function (str, p1)
|
||||
{
|
||||
return egw.lang(p1);
|
||||
});
|
||||
@ -682,20 +746,26 @@ export class et2_widget extends ClassWithAttributes
|
||||
|
||||
// Check to see if modifications change type
|
||||
var modifications = this.getArrayMgr("modifications");
|
||||
if (modifications && _node.getAttribute("id")) {
|
||||
var entry : any = modifications.getEntry(_node.getAttribute("id"));
|
||||
if (entry == null) {
|
||||
if (modifications && _node.getAttribute("id"))
|
||||
{
|
||||
var entry: any = modifications.getEntry(_node.getAttribute("id"));
|
||||
if (entry == null)
|
||||
{
|
||||
// Try again, but skip the fancy stuff
|
||||
// TODO: Figure out why the getEntry() call doesn't always work
|
||||
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);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try the root, in case a namespace got missed
|
||||
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;
|
||||
}
|
||||
entry = null;
|
||||
@ -703,25 +773,27 @@ export class et2_widget extends ClassWithAttributes
|
||||
|
||||
// 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!
|
||||
if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0) {
|
||||
if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0)
|
||||
{
|
||||
_nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName);
|
||||
}
|
||||
|
||||
// Get the constructor - if the widget is readonly, use the special "_ro"
|
||||
// constructor if it is available
|
||||
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"];
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if(undefined == window.customElements.get(_nodeName))
|
||||
{
|
||||
// Creates the new widget, passes this widget as an instance and
|
||||
// passes the widgetType. Then it goes on loading the XML for it.
|
||||
var widget = new constructor(this, attributes);
|
||||
@ -731,30 +803,8 @@ export class et2_widget extends ClassWithAttributes
|
||||
}
|
||||
else
|
||||
{
|
||||
widget = this.loadWebComponent(_nodeName, _node, attributes);
|
||||
|
||||
if(this.addChild)
|
||||
{
|
||||
// webcomponent going into old et2_widget
|
||||
this.addChild(widget);
|
||||
widget = loadWebComponent(_nodeName, _node, this);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@ -766,16 +816,20 @@ export class et2_widget extends ClassWithAttributes
|
||||
loadFromXML(_node)
|
||||
{
|
||||
// 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 widgetType = node.nodeName.toLowerCase();
|
||||
|
||||
if (widgetType == "#comment") {
|
||||
if (widgetType == "#comment")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (widgetType == "#text") {
|
||||
if (node.data.replace(/^\s+|\s+$/g, '')) {
|
||||
if (widgetType == "#text")
|
||||
{
|
||||
if (node.data.replace(/^\s+|\s+$/g, ''))
|
||||
{
|
||||
this.loadContent(node.data);
|
||||
}
|
||||
continue;
|
||||
@ -823,29 +877,39 @@ export class et2_widget extends ClassWithAttributes
|
||||
|
||||
// Make sure promises is defined to avoid errors.
|
||||
// We'll warn (below) if programmer should have passed it.
|
||||
if (typeof promises == "undefined") {
|
||||
if (typeof promises == "undefined")
|
||||
{
|
||||
promises = [];
|
||||
var warn_if_deferred = true;
|
||||
}
|
||||
|
||||
var loadChildren = function () {
|
||||
var loadChildren = function ()
|
||||
{
|
||||
// Descend recursively into the tree
|
||||
for (var i = 0; i < this._children.length; i++) {
|
||||
try {
|
||||
for (var i = 0; i < this._children.length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = this.doLoadingFinished();
|
||||
if (typeof result == "boolean" && result) {
|
||||
if (typeof result == "boolean" && result)
|
||||
{
|
||||
// Simple widget finishes nicely
|
||||
loadChildren.apply(this, arguments);
|
||||
} else if (typeof result == "object" && result.done) {
|
||||
}
|
||||
else if (typeof result == "object" && result.done)
|
||||
{
|
||||
// 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
|
||||
egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this);
|
||||
}
|
||||
@ -868,11 +932,14 @@ export class et2_widget extends ClassWithAttributes
|
||||
*/
|
||||
initAttributes(_attrs)
|
||||
{
|
||||
for (var key in _attrs) {
|
||||
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) {
|
||||
for (var key in _attrs)
|
||||
{
|
||||
if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined))
|
||||
{
|
||||
var val = _attrs[key];
|
||||
// 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,
|
||||
this.implements(et2_IInputNode) ? (<et2_inputWidget><unknown>this).getInputNode() :
|
||||
(this.implements(et2_IDOMNode) ? (<et2_IDOMNode><unknown>this).getDOMNode() : null));
|
||||
@ -891,7 +958,7 @@ export class et2_widget extends ClassWithAttributes
|
||||
*
|
||||
* @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise}
|
||||
*/
|
||||
doLoadingFinished() : JQueryPromise<any> | boolean
|
||||
doLoadingFinished(): JQueryPromise<any> | boolean
|
||||
{
|
||||
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"
|
||||
* widget using the setApiInstance function.
|
||||
*/
|
||||
egw() : IegwAppLocal
|
||||
egw(): IegwAppLocal
|
||||
{
|
||||
// The _egw property is not set
|
||||
if (typeof this._egw === 'undefined') {
|
||||
if (this._parent != null) {
|
||||
if (typeof this._egw === 'undefined')
|
||||
{
|
||||
if (this._parent != null)
|
||||
{
|
||||
return this._parent.egw();
|
||||
}
|
||||
|
||||
// Get the window this object belongs to
|
||||
var wnd = null;
|
||||
if (this.implements(et2_IDOMNode)) {
|
||||
if (this.implements(et2_IDOMNode))
|
||||
{
|
||||
var node = (<et2_IDOMNode><unknown>this).getDOMNode();
|
||||
if (node && node.ownerDocument) {
|
||||
if (node && node.ownerDocument)
|
||||
{
|
||||
wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView;
|
||||
}
|
||||
}
|
||||
@ -933,7 +1004,7 @@ export class et2_widget extends ClassWithAttributes
|
||||
*
|
||||
* @param {IegwAppLocal} _egw egw object to set
|
||||
*/
|
||||
setApiInstance(_egw : IegwAppLocal)
|
||||
setApiInstance(_egw: IegwAppLocal)
|
||||
{
|
||||
this._egw = _egw;
|
||||
}
|
||||
@ -956,22 +1027,26 @@ export class et2_widget extends ClassWithAttributes
|
||||
*
|
||||
* @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 = {};
|
||||
}
|
||||
|
||||
// Add all managers of this object to the result, if they have not already
|
||||
// been set in the result
|
||||
for (var key in this._mgrs) {
|
||||
if (typeof _mgrs[key] == "undefined") {
|
||||
for (var key in this._mgrs)
|
||||
{
|
||||
if (typeof _mgrs[key] == "undefined")
|
||||
{
|
||||
_mgrs[key] = this._mgrs[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively applies this function to the parent widget
|
||||
if (this._parent) {
|
||||
if (this._parent)
|
||||
{
|
||||
this._parent.getArrayMgrs(_mgrs);
|
||||
}
|
||||
|
||||
@ -984,7 +1059,7 @@ export class et2_widget extends ClassWithAttributes
|
||||
* @param {string} _part which array mgr to set
|
||||
* @param {object} _mgr
|
||||
*/
|
||||
setArrayMgr(_part : string, _mgr : et2_arrayMgr)
|
||||
setArrayMgr(_part: string, _mgr: et2_arrayMgr)
|
||||
{
|
||||
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
|
||||
*/
|
||||
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];
|
||||
} else if (this._parent) {
|
||||
}
|
||||
else if (this._parent)
|
||||
{
|
||||
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
|
||||
*/
|
||||
checkCreateNamespace(_attrs? : any)
|
||||
checkCreateNamespace(_attrs?: any)
|
||||
{
|
||||
// Get the content manager
|
||||
var mgrs = this.getArrayMgrs();
|
||||
|
||||
for (var key in mgrs) {
|
||||
for (var key in mgrs)
|
||||
{
|
||||
var mgr = mgrs[key];
|
||||
|
||||
// Get the original content manager if we have already created a
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Check whether the manager has a namespace for the id of this object
|
||||
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
|
||||
// create an own perspective.
|
||||
this._mgrs[key] = mgr.openPerspective(this, this.id);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// The current content manager does not have an own namespace for
|
||||
// this element, so use the content manager of the parent.
|
||||
delete (this._mgrs[key]);
|
||||
@ -1048,7 +1131,7 @@ export class et2_widget extends ClassWithAttributes
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
protected _createNamespace() : boolean
|
||||
protected _createNamespace(): boolean
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -1077,9 +1160,12 @@ export class et2_widget extends ClassWithAttributes
|
||||
*/
|
||||
getInstanceManager()
|
||||
{
|
||||
if (this._inst != null) {
|
||||
if (this._inst != null)
|
||||
{
|
||||
return this._inst;
|
||||
} else if (this._parent) {
|
||||
}
|
||||
else if (this._parent)
|
||||
{
|
||||
return this._parent.getInstanceManager();
|
||||
}
|
||||
|
||||
|
@ -1759,17 +1759,9 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if(typeof _row[x] != "undefined" && _row[x].widget)
|
||||
{
|
||||
|
||||
columnWidgets[x] = _row[x].widget;
|
||||
|
||||
// Append the widget to this container
|
||||
this.addChild(_row[x].widget);
|
||||
}
|
||||
else
|
||||
{
|
||||
columnWidgets[x] = _row[x].widget;
|
||||
}
|
||||
// Pass along column alignment
|
||||
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];
|
||||
}
|
||||
}
|
||||
if(_sender == this.et2_searchbox) return this.search_box[0];
|
||||
if(_sender.id == 'export') return this.right_div[0];
|
||||
if(_sender == this.et2_searchbox)
|
||||
{
|
||||
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")
|
||||
{
|
||||
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;
|
||||
|
@ -193,7 +193,7 @@ export class et2_nextmatch_rowProvider
|
||||
}
|
||||
|
||||
// Adjust data for that row
|
||||
entry.widget.transformAttributes.call(entry.widget,data);
|
||||
entry.widget.transformAttributes?.call(entry.widget, data);
|
||||
|
||||
// Call the setDetachedAttributes function
|
||||
entry.widget.setDetachedAttributes(nodes, data, _data);
|
||||
@ -282,22 +282,35 @@ export class et2_nextmatch_rowProvider
|
||||
// Get all attribute values
|
||||
for (const key in _widget.attributes)
|
||||
{
|
||||
if(typeof _widget.attributes[key] !== "object")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let attr_name = key;
|
||||
let val;
|
||||
if(!_widget.attributes[key].ignore &&
|
||||
typeof _widget.options != "undefined" &&
|
||||
typeof _widget.options[key] != "undefined")
|
||||
{
|
||||
const val = _widget.options[key];
|
||||
|
||||
val = _widget.options[key];
|
||||
}
|
||||
// Handle web components
|
||||
else if(_widget.attributes[key].value)
|
||||
{
|
||||
val = _widget.attributes[key].value;
|
||||
attr_name = _widget.attributes[key].name;
|
||||
}
|
||||
// TODO: Improve detection
|
||||
if(typeof val == "string" && val.indexOf("$") >= 0)
|
||||
{
|
||||
hasAttr = true;
|
||||
widgetData.data.push({
|
||||
"attribute": key,
|
||||
"attribute": attr_name,
|
||||
"expression": val
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the entry if there is any data in it
|
||||
if(hasAttr)
|
||||
|
@ -66,7 +66,7 @@ export class et2_box extends et2_baseWidget implements et2_IDetachedDOM
|
||||
* @param {object} _node
|
||||
*/
|
||||
loadFromXML(_node) {
|
||||
if(this.getType() != "box")
|
||||
if(this.getType() != "old-box")
|
||||
{
|
||||
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
|
||||
|
@ -20,7 +20,7 @@ import {et2_register_widget, WidgetConfig} from "./et2_core_widget";
|
||||
import {et2_baseWidget} from './et2_core_baseWidget'
|
||||
import {et2_inputWidget} from "./et2_core_inputWidget";
|
||||
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";
|
||||
|
||||
/**
|
||||
@ -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 && for_widget.id)
|
||||
{
|
||||
if(for_widget.dom_id)
|
||||
if(for_widget.dom_id || for_widget.getDOMNode().id)
|
||||
{
|
||||
for_id = for_widget.dom_id;
|
||||
if(for_widget.instanceOf(et2_inputWidget) && for_widget.getInputNode() && for_widget.dom_id !== for_widget.getInputNode().id)
|
||||
for_id = for_widget.dom_id || for_widget.getDOMNode().id;
|
||||
if(for_widget.instanceOf(et2_IInputNode) && for_widget.getInputNode() && for_id !== for_widget.getInputNode().id)
|
||||
{
|
||||
for_id = for_widget.getInputNode().id;
|
||||
}
|
||||
|
@ -96,14 +96,14 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
description: "Defines sortable start callback function"
|
||||
}
|
||||
};
|
||||
private table: JQuery;
|
||||
private thead: JQuery;
|
||||
private tfoot: JQuery;
|
||||
private tbody: JQuery;
|
||||
private table : JQuery;
|
||||
private thead : JQuery;
|
||||
private tfoot : JQuery;
|
||||
private tbody : JQuery;
|
||||
|
||||
// Counters for rows and columns
|
||||
private rowCount: number = 0;
|
||||
private columnCount: number = 0;
|
||||
private rowCount : number = 0;
|
||||
private columnCount : number = 0;
|
||||
|
||||
// 2D-Array which holds references to the DOM td tags
|
||||
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
|
||||
private wrapper = null;
|
||||
private lastRowNode: null;
|
||||
private lastRowNode : null;
|
||||
|
||||
private sortablejs : Sortable = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -139,19 +140,20 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
.appendTo(this.table);
|
||||
}
|
||||
|
||||
_initCells(_colData : ColumnEntry[], _rowData : RowEntry[]) {
|
||||
_initCells(_colData : ColumnEntry[], _rowData : RowEntry[])
|
||||
{
|
||||
// Copy the width and height
|
||||
const w = _colData.length;
|
||||
const h = _rowData.length;
|
||||
|
||||
// Create the 2D-Cells array
|
||||
const cells = new Array(h);
|
||||
for (let y = 0; y < h; y++)
|
||||
for(let y = 0; y < h; y++)
|
||||
{
|
||||
cells[y] = new Array(w);
|
||||
|
||||
// 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
|
||||
const col_disabled = _colData[x].disabled;
|
||||
@ -200,10 +202,10 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
|
||||
_getCell(_cells, _x, _y)
|
||||
{
|
||||
if ((0 <= _y) && (_y < _cells.length))
|
||||
if((0 <= _y) && (_y < _cells.length))
|
||||
{
|
||||
const row = _cells[_y];
|
||||
if ((0 <= _x) && (_x < row.length))
|
||||
if((0 <= _x) && (_x < row.length))
|
||||
{
|
||||
return row[_x];
|
||||
}
|
||||
@ -214,7 +216,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
|
||||
_forceNumber(_val)
|
||||
{
|
||||
if (isNaN(_val))
|
||||
if(isNaN(_val))
|
||||
{
|
||||
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
|
||||
var nm = false;
|
||||
let widget: et2_widget = this;
|
||||
let widget : et2_widget = this;
|
||||
while(!nm && widget != this.getRoot())
|
||||
{
|
||||
nm = (widget.getType() == 'nextmatch');
|
||||
@ -234,14 +236,15 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
}
|
||||
|
||||
// Parse the columns tag
|
||||
et2_filteredNodeIterator(columns, function(node, nodeName) {
|
||||
et2_filteredNodeIterator(columns, function(node, nodeName)
|
||||
{
|
||||
const colDataEntry = this._getColDataEntry();
|
||||
// This cannot be done inside a nm, it will expand it later
|
||||
colDataEntry["disabled"] = nm ?
|
||||
et2_readAttrWithDefault(node, "disabled", "") :
|
||||
this.getArrayMgr("content")
|
||||
.parseBoolExpression(et2_readAttrWithDefault(node, "disabled", ""));
|
||||
if (nodeName == "column")
|
||||
if(nodeName == "column")
|
||||
{
|
||||
colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto");
|
||||
colDataEntry["class"] = et2_readAttrWithDefault(node, "class", "");
|
||||
@ -266,11 +269,12 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
}, this);
|
||||
|
||||
// Parse the rows tag
|
||||
et2_filteredNodeIterator(rows, function(node, nodeName) {
|
||||
et2_filteredNodeIterator(rows, function(node, nodeName)
|
||||
{
|
||||
const rowDataEntry = this._getRowDataEntry();
|
||||
rowDataEntry["disabled"] = this.getArrayMgr("content")
|
||||
.parseBoolExpression(et2_readAttrWithDefault(node, "disabled", ""));
|
||||
if (nodeName == "row")
|
||||
if(nodeName == "row")
|
||||
{
|
||||
// Remember this row for auto-repeat - it'll eventually be the last one
|
||||
this.lastRowNode = node;
|
||||
@ -300,8 +304,8 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
if(this.getArrayMgr("content"))
|
||||
{
|
||||
const content = this.getArrayMgr("content");
|
||||
var rowDataEntry = rowData[rowData.length-1];
|
||||
rowIndex = rowData.length-1;
|
||||
var rowDataEntry = rowData[rowData.length - 1];
|
||||
rowIndex = rowData.length - 1;
|
||||
// Find out if we have any content rows, and how many
|
||||
let cont = true;
|
||||
while(cont)
|
||||
@ -313,7 +317,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
else if (this.lastRowNode != null)
|
||||
else if(this.lastRowNode != null)
|
||||
{
|
||||
// Have to look through actual widgets to support const[$row]
|
||||
// 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
|
||||
const currentPerspective = jQuery.extend({}, content.perspectiveData);
|
||||
const check = function (node, nodeName) {
|
||||
if (nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox') {
|
||||
const check = function(node, nodeName)
|
||||
{
|
||||
if(nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox')
|
||||
{
|
||||
return et2_filteredNodeIterator(node, check, this);
|
||||
}
|
||||
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, "");
|
||||
// Don't include first char, those should be handled by normal means
|
||||
// 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.
|
||||
let ident = content.expandName(value);
|
||||
// expandName() handles index into content (@), but we have to look up
|
||||
// regular values
|
||||
if (value[0] != '@') {
|
||||
if(value[0] != '@')
|
||||
{
|
||||
// Returns null if there isn't an actual value
|
||||
ident = content.getEntry(ident, false, true);
|
||||
}
|
||||
while (ident != null && rowIndex < 1000) {
|
||||
while(ident != null && rowIndex < 1000)
|
||||
{
|
||||
rowData[rowIndex] = jQuery.extend({}, rowDataEntry);
|
||||
content.perspectiveData.row = ++rowIndex;
|
||||
ident = content.expandName(value);
|
||||
if (value[0] != '@') {
|
||||
if(value[0] != '@')
|
||||
{
|
||||
// Returns null if there isn't an actual value
|
||||
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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
et2_filteredNodeIterator(this.lastRowNode, check,this);
|
||||
et2_filteredNodeIterator(this.lastRowNode, check, this);
|
||||
cont = false;
|
||||
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 w = (h > 0) ? cells[0].length : 0;
|
||||
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
|
||||
let x = 0;
|
||||
|
||||
et2_filteredNodeIterator(columns, function(node, nodeName) {
|
||||
et2_filteredNodeIterator(columns, function(node, nodeName)
|
||||
{
|
||||
|
||||
function _readColNode(node, nodeName) {
|
||||
if (y >= h)
|
||||
function _readColNode(node, nodeName)
|
||||
{
|
||||
if(y >= h)
|
||||
{
|
||||
this.egw().debug("warn", "Skipped grid cell in column, '" +
|
||||
nodeName + "'");
|
||||
@ -402,7 +417,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
const cell = this._getCell(cells, x, y);
|
||||
|
||||
// Read the span value of the element
|
||||
if (node.getAttribute("span"))
|
||||
if(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;
|
||||
}
|
||||
|
||||
if (cell.rowSpan == "all")
|
||||
if(cell.rowSpan == "all")
|
||||
{
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -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
|
||||
// the column
|
||||
var y = 0;
|
||||
if (nodeName == "column")
|
||||
if(nodeName == "column")
|
||||
{
|
||||
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');
|
||||
widget = widget.getParent();
|
||||
}
|
||||
et2_filteredNodeIterator(rows, function(node, nodeName) {
|
||||
et2_filteredNodeIterator(rows, function(node, nodeName)
|
||||
{
|
||||
|
||||
readRowNode = function _readRowNode(node, nodeName) {
|
||||
if (x >= w)
|
||||
readRowNode = function _readRowNode(node, nodeName)
|
||||
{
|
||||
if(x >= w)
|
||||
{
|
||||
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);
|
||||
|
||||
// Read the span value of the element
|
||||
if (node.getAttribute("span"))
|
||||
if(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;
|
||||
}
|
||||
|
||||
if (cell.colSpan == "all")
|
||||
if(cell.colSpan == "all")
|
||||
{
|
||||
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);
|
||||
|
||||
// Read the align value of the element
|
||||
if (node.getAttribute("align"))
|
||||
if(node.getAttribute("align"))
|
||||
{
|
||||
cell.align = node.getAttribute("align");
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
@ -540,10 +557,10 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (cell.widget == null)
|
||||
if(cell.widget == null)
|
||||
{
|
||||
cell.widget = widget;
|
||||
}
|
||||
@ -562,7 +579,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (nodeName == "row")
|
||||
if(nodeName == "row")
|
||||
{
|
||||
// Adjust for the row
|
||||
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;
|
||||
}
|
||||
|
||||
let cell = this._getCell(cells, x,y);
|
||||
let cell = this._getCell(cells, x, y);
|
||||
if(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);
|
||||
|
||||
// Extra content rows
|
||||
for(y; y < h; y++) {
|
||||
for(y; y < h; y++)
|
||||
{
|
||||
x = 0;
|
||||
|
||||
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
|
||||
// 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];
|
||||
|
||||
if (cell.widget != null)
|
||||
if(cell.widget != null)
|
||||
{
|
||||
if (cell.autoColSpan)
|
||||
if(cell.autoColSpan)
|
||||
{
|
||||
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
|
||||
// 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];
|
||||
|
||||
if (cell.widget != null)
|
||||
if(cell.widget != null)
|
||||
{
|
||||
if (cell.autoRowSpan)
|
||||
if(cell.autoRowSpan)
|
||||
{
|
||||
cell.rowSpan = h - y;
|
||||
}
|
||||
@ -655,6 +673,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* As the does not fit very well into the default widget structure, we're
|
||||
* 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
|
||||
*/
|
||||
loadFromXML(_node ) {
|
||||
loadFromXML(_node)
|
||||
{
|
||||
// Keep the node for later changing / reloading
|
||||
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 columnsElems = et2_directChildrenByTagName(_node, "columns");
|
||||
|
||||
if (rowsElems.length == 1 && columnsElems.length == 1)
|
||||
if(rowsElems.length == 1 && columnsElems.length == 1)
|
||||
{
|
||||
const columns = columnsElems[0];
|
||||
const rows = rowsElems[0];
|
||||
const colData: ColumnEntry[] = [];
|
||||
const rowData: RowEntry[] = [];
|
||||
const colData : ColumnEntry[] = [];
|
||||
const rowData : RowEntry[] = [];
|
||||
|
||||
// Fetch the column and row data
|
||||
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.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;
|
||||
|
||||
// Create the table rows.
|
||||
for (let y = 0; y < h; y++)
|
||||
for(let y = 0; y < h; y++)
|
||||
{
|
||||
let parent = this.tbody;
|
||||
switch(this.rowData[y]["part"])
|
||||
{
|
||||
case 'header':
|
||||
if (!this.tbody.children().length && !this.tfoot.children().length)
|
||||
if(!this.tbody.children().length && !this.tfoot.children().length)
|
||||
{
|
||||
parent = this.thead;
|
||||
}
|
||||
break;
|
||||
case 'footer':
|
||||
if (!this.tbody.children().length)
|
||||
if(!this.tbody.children().length)
|
||||
{
|
||||
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)
|
||||
.addClass(this.rowData[y]["class"]);
|
||||
|
||||
if (this.rowData[y].disabled)
|
||||
if(this.rowData[y].disabled)
|
||||
{
|
||||
tr.hide();
|
||||
}
|
||||
|
||||
if (this.rowData[y].height != "auto")
|
||||
if(this.rowData[y].height != "auto")
|
||||
{
|
||||
tr.height(this.rowData[y].height);
|
||||
}
|
||||
|
||||
if (this.rowData[y].valign)
|
||||
if(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
|
||||
// cell.
|
||||
for (let x = 0; x < w;)
|
||||
for(let x = 0; x < w;)
|
||||
{
|
||||
// Fetch a cell from the cells
|
||||
const cell = this._getCell(_cells, x, y);
|
||||
|
||||
if (cell.td == null && cell.widget != null)
|
||||
if(cell.td == null && cell.widget != null)
|
||||
{
|
||||
// Create the cell
|
||||
const td = jQuery(document.createElement("td")).appendTo(tr)
|
||||
.addClass(cell["class"]);
|
||||
|
||||
if (cell.disabled)
|
||||
if(cell.disabled)
|
||||
{
|
||||
td.hide();
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
@ -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);
|
||||
|
||||
// Set the col and row span values
|
||||
if (cs > 1) {
|
||||
if(cs > 1)
|
||||
{
|
||||
td.attr("colspan", cs);
|
||||
}
|
||||
|
||||
if (rs > 1) {
|
||||
if(rs > 1)
|
||||
{
|
||||
td.attr("rowspan", rs);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -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
|
||||
// outer table.
|
||||
if (_sender == this || typeof _sender == 'undefined')
|
||||
if(_sender == this || typeof _sender == 'undefined')
|
||||
{
|
||||
return this.wrapper != null ? this.wrapper[0] : this.table[0];
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -839,18 +869,18 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
return null;
|
||||
}
|
||||
|
||||
isInTree(_sender?:et2_widget) : boolean
|
||||
isInTree(_sender? : et2_widget) : boolean
|
||||
{
|
||||
let vis = true;
|
||||
|
||||
if (typeof _sender != "undefined" && _sender != this)
|
||||
if(typeof _sender != "undefined" && _sender != this)
|
||||
{
|
||||
vis = false;
|
||||
|
||||
// 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' ?
|
||||
this.managementArray[i].disabled :
|
||||
@ -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'
|
||||
*/
|
||||
set_overflow(_value: string)
|
||||
set_overflow(_value : string)
|
||||
{
|
||||
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')
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
if(wrapper.length && (!_value || _value === 'visible')) {
|
||||
if(wrapper.length && (!_value || _value === 'visible'))
|
||||
{
|
||||
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.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
|
||||
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];
|
||||
if(cell.widget)
|
||||
{
|
||||
this.removeChild(cell.widget);
|
||||
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
|
||||
*/
|
||||
set_sortable(sortable: boolean | Function)
|
||||
set_sortable(sortable : boolean | Function)
|
||||
{
|
||||
const self = this;
|
||||
let tbody = this.getDOMNode().getElementsByTagName('tbody')[0];
|
||||
@ -954,32 +986,37 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
this.sortablejs = new Sortable(tbody,{
|
||||
this.sortablejs = new Sortable(tbody, {
|
||||
group: this.options.sortable_connectWith,
|
||||
draggable: "tr:not(.th)",
|
||||
filter: this.options.sortable_cancel,
|
||||
ghostClass: this.options.sortable_placeholder,
|
||||
dataIdAttr: 'id',
|
||||
onAdd:function (event) {
|
||||
if (typeof self.options.sortable_recieveCallback == 'function') {
|
||||
onAdd: function(event)
|
||||
{
|
||||
if(typeof self.options.sortable_recieveCallback == 'function')
|
||||
{
|
||||
self.options.sortable_recieveCallback.call(self, event, this, self.id);
|
||||
}
|
||||
},
|
||||
onStart: function (event, ui) {
|
||||
if (typeof self.options.sortable_startCallback == 'function') {
|
||||
onStart: function(event, ui)
|
||||
{
|
||||
if(typeof self.options.sortable_startCallback == 'function')
|
||||
{
|
||||
self.options.sortable_startCallback.call(self, event, this, self.id);
|
||||
}
|
||||
},
|
||||
onSort: function (event) {
|
||||
self.egw().json(sortable,[
|
||||
onSort: function(event)
|
||||
{
|
||||
self.egw().json(sortable, [
|
||||
self.getInstanceManager().etemplate_exec_id,
|
||||
self.sortablejs.toArray(),
|
||||
self.id],
|
||||
@ -1002,9 +1039,10 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
// get appObjectManager for the actual app, it might not always be the current app(e.g. running app content under admin tab)
|
||||
// @ts-ignore
|
||||
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);
|
||||
if (widget_object == null) {
|
||||
if(widget_object == null)
|
||||
{
|
||||
// Add a new container to the object manager which will hold the widget
|
||||
// objects
|
||||
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;
|
||||
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);
|
||||
if(content)
|
||||
{
|
||||
@ -1073,33 +1111,33 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
_getColumnName()
|
||||
{
|
||||
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];
|
||||
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('_');
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// apply the ratio
|
||||
_height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height;
|
||||
if (_height != 0)
|
||||
_height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height;
|
||||
if(_height != 0)
|
||||
{
|
||||
if (this.wrapper)
|
||||
if(this.wrapper)
|
||||
{
|
||||
this.wrapper.height(this.wrapper.height() + _height);
|
||||
}
|
||||
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
|
||||
{
|
||||
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];
|
||||
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;
|
||||
if (!found) row[c].widget.iterateOver(function(_widget) {if (_widget === _sender) found = true;});
|
||||
if (found)
|
||||
if(!found) row[c].widget.iterateOver(function(_widget)
|
||||
{
|
||||
if(_widget === _sender) found = true;
|
||||
});
|
||||
if(found)
|
||||
{
|
||||
// return a fake row object allowing to iterate over it's children
|
||||
const row_obj: et2_widget = new et2_widget(this, {});
|
||||
for(var c=0; c < row.length; ++c)
|
||||
const row_obj : et2_widget = new et2_widget(this, {});
|
||||
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);
|
||||
// we must not free the children!
|
||||
row_obj.destroy = function(){
|
||||
row_obj.destroy = function()
|
||||
{
|
||||
// @ts-ignore
|
||||
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...
|
||||
*/
|
||||
get_align(): string {
|
||||
get_align() : string
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
et2_register_widget(et2_grid, ["grid"]);
|
||||
|
||||
interface ColumnEntry
|
||||
{
|
||||
id? : string,
|
||||
width: string | number, // 'auto'
|
||||
class: string, // "",
|
||||
align: string, // "",
|
||||
span: string | number // "1",
|
||||
disabled: boolean // false
|
||||
width : string | number, // 'auto'
|
||||
class : string, // "",
|
||||
align : string, // "",
|
||||
span : string | number // "1",
|
||||
disabled : boolean // false
|
||||
}
|
||||
|
||||
interface RowEntry
|
||||
{
|
||||
id? : string
|
||||
height: string | number, // "auto",
|
||||
class: string, // "",
|
||||
valign: string, // "top",
|
||||
span: string | number, // "1",
|
||||
disabled: boolean // false
|
||||
height : string | number, // "auto",
|
||||
class : string, // "",
|
||||
valign : string, // "top",
|
||||
span : string | number, // "1",
|
||||
disabled : boolean // false
|
||||
}
|
@ -199,6 +199,6 @@ export class et2_hbox extends et2_baseWidget
|
||||
}
|
||||
}
|
||||
}
|
||||
et2_register_widget(et2_hbox, ["hbox"]);
|
||||
et2_register_widget(et2_hbox, ["old-hbox"]);
|
||||
|
||||
|
||||
|
@ -23,7 +23,12 @@ import {et2_nextmatch, et2_nextmatch_header_bar} from "./et2_extension_nextmatch
|
||||
import {et2_tabbox} from "./et2_widget_tabs";
|
||||
import '../jsapi/egw_json.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*/
|
||||
import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle
|
||||
import './et2_widget_template';
|
||||
@ -113,7 +118,7 @@ export class etemplate2
|
||||
private app_obj: EgwApp;
|
||||
app: string;
|
||||
|
||||
constructor(_container : HTMLElement, _menuaction? : string, _uniqueId?: string)
|
||||
constructor(_container: HTMLElement, _menuaction?: string, _uniqueId?: string)
|
||||
{
|
||||
if (typeof _menuaction == "undefined")
|
||||
{
|
||||
@ -165,13 +170,15 @@ export class etemplate2
|
||||
*
|
||||
* @param {jQuery.event} e
|
||||
*/
|
||||
public resize(e) {
|
||||
public resize(e)
|
||||
{
|
||||
const event = e;
|
||||
const self = this;
|
||||
let excess_height: number | boolean = false;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -200,7 +207,7 @@ export class etemplate2
|
||||
|
||||
// If we're visible, call the "resize" event of all functions which implement the
|
||||
// "IResizeable" interface
|
||||
if(jQuery(self.DOMContainer).is(":visible"))
|
||||
if (jQuery(self.DOMContainer).is(":visible"))
|
||||
{
|
||||
self._widgetContainer.iterateOver(function (_widget)
|
||||
{
|
||||
@ -227,7 +234,7 @@ export class etemplate2
|
||||
* @param _keep_app_object keep app object
|
||||
* @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');
|
||||
|
||||
@ -267,11 +274,11 @@ export class etemplate2
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
@ -353,7 +360,7 @@ export class etemplate2
|
||||
bind_unload()
|
||||
{
|
||||
// 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);
|
||||
window.addEventListener("beforeunload", this.close_prompt);
|
||||
@ -371,9 +378,9 @@ export class etemplate2
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@ -507,12 +514,15 @@ export class etemplate2
|
||||
|
||||
// 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
|
||||
if (Array.isArray(_data.langRequire)) {
|
||||
if (Array.isArray(_data.langRequire))
|
||||
{
|
||||
promisses.push(egw(currentapp, window).langRequire(window, _data.langRequire));
|
||||
}
|
||||
return Promise.all(promisses).catch((err) => {
|
||||
console.log("et2.load(): error loading lang-files and app.js: "+err.message);
|
||||
}).then(() => {
|
||||
return Promise.all(promisses).catch((err) =>
|
||||
{
|
||||
console.log("et2.load(): error loading lang-files and app.js: " + err.message);
|
||||
}).then(() =>
|
||||
{
|
||||
this.clear();
|
||||
|
||||
// Initialize application js
|
||||
@ -564,7 +574,8 @@ export class etemplate2
|
||||
const _load = function ()
|
||||
{
|
||||
egw.debug("log", "Loading template...");
|
||||
if (egw.debug_level() >= 4 && console.timeStamp) {
|
||||
if (egw.debug_level() >= 4 && console.timeStamp)
|
||||
{
|
||||
console.timeStamp("Begin rendering template");
|
||||
}
|
||||
|
||||
@ -670,17 +681,19 @@ export class etemplate2
|
||||
// Profiling
|
||||
if (egw.debug_level() >= 4)
|
||||
{
|
||||
if (console.timeEnd) {
|
||||
if (console.timeEnd)
|
||||
{
|
||||
console.timeEnd(_name);
|
||||
}
|
||||
if (console.profileEnd) {
|
||||
if (console.profileEnd)
|
||||
{
|
||||
console.profileEnd(_name);
|
||||
}
|
||||
const end_time = (new Date).getTime();
|
||||
let gen_time_div = jQuery('#divGenTime_' + appname);
|
||||
if (!gen_time_div.length) gen_time_div = jQuery('.pageGenTime');
|
||||
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));
|
||||
};
|
||||
@ -819,7 +832,8 @@ export class etemplate2
|
||||
indexes = [indexes.shift(), indexes.join('[')];
|
||||
indexes[1] = indexes[1].substring(0, indexes[1].length - 1);
|
||||
const children = indexes[1].split('][');
|
||||
if (children.length) {
|
||||
if (children.length)
|
||||
{
|
||||
indexes = jQuery.merge([indexes[0]], children);
|
||||
}
|
||||
}
|
||||
@ -1009,7 +1023,8 @@ export class etemplate2
|
||||
_root.iterateOver(function (_widget)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -1024,7 +1039,8 @@ export class etemplate2
|
||||
indexes = [indexes.shift(), indexes.join('[')];
|
||||
indexes[1] = indexes[1].substring(0, indexes[1].length - 1);
|
||||
const children = indexes[1].split('][');
|
||||
if (children.length) {
|
||||
if (children.length)
|
||||
{
|
||||
indexes = jQuery.merge([indexes[0]], children);
|
||||
}
|
||||
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
|
||||
* @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 app = _app.split('-');
|
||||
@ -1260,7 +1276,7 @@ export class etemplate2
|
||||
* @param {string} id DOM ID of the container node
|
||||
* @returns {etemplate2|null}
|
||||
*/
|
||||
public static getById (id)
|
||||
public static getById(id)
|
||||
{
|
||||
for (let name in etemplate2._byTemplate)
|
||||
{
|
||||
@ -1425,38 +1441,48 @@ export class etemplate2
|
||||
* @returns {Boolean} Handled by this plugin
|
||||
* @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
|
||||
|
||||
//Check whether all needed parameters have been passed and call the alertHandler function
|
||||
if ((typeof res.data.id != 'undefined') &&
|
||||
(typeof res.data.key != 'undefined') &&
|
||||
(typeof res.data.value != '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
|
||||
return false;
|
||||
}
|
||||
if (res.data.key == 'etemplate_exec_id') {
|
||||
if (res.data.key == 'etemplate_exec_id')
|
||||
{
|
||||
this._etemplate_exec_id = res.data.value;
|
||||
return true;
|
||||
}
|
||||
if (this._widgetContainer == null) {
|
||||
if (this._widgetContainer == null)
|
||||
{
|
||||
// Right etemplate, but it's already been cleared.
|
||||
egw.debug('warn', "Tried to call assign on an un-loaded etemplate", res.data);
|
||||
return false;
|
||||
}
|
||||
const widget = this._widgetContainer.getWidgetById(res.data.id);
|
||||
if (widget) {
|
||||
if (typeof widget['set_' + res.data.key] != 'function') {
|
||||
if (widget)
|
||||
{
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
try
|
||||
{
|
||||
widget['set_' + res.data.key].call(widget, res.data.value);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
* no longer needed, in order to prevent memory leaks.
|
||||
*
|
||||
* @param _elem is the element to which the tooltip should get bound. It
|
||||
* has to be a jQuery node.
|
||||
* @param _elem is the element to which the tooltip should get bound.
|
||||
* @param _html is the html code which should be shown as tooltip.
|
||||
*/
|
||||
tooltipBind: function(_elem, _html, _isHtml) {
|
||||
_elem = jQuery(_elem);
|
||||
if (_html != '')
|
||||
{
|
||||
_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.
|
||||
*/
|
||||
tooltipUnbind: function(_elem) {
|
||||
_elem = jQuery(_elem);
|
||||
if (current_elem == _elem)
|
||||
{
|
||||
hide();
|
||||
|
@ -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'));
|
||||
|
@ -60,7 +60,7 @@ class Date extends Transformer
|
||||
* @param string $cname
|
||||
* @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')
|
||||
{
|
||||
@ -87,14 +87,20 @@ class Date extends Transformer
|
||||
* @param array $expand
|
||||
* @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);
|
||||
$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)
|
||||
{
|
||||
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
|
||||
if ($value instanceof \DateTime)
|
||||
if($value instanceof \DateTime)
|
||||
{
|
||||
$date = Api\DateTime::server2user($value);
|
||||
}
|
||||
@ -142,58 +151,65 @@ class Date extends Transformer
|
||||
* @param string $cname current namespace
|
||||
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
|
||||
* @param array $content
|
||||
* @param array &$validated=array() validated content
|
||||
* @param array &$validated =array() validated content
|
||||
* @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);
|
||||
|
||||
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);
|
||||
$valid =& self::get_array($validated, $form_name, true);
|
||||
|
||||
if ($value && $this->type !== 'date-duration')
|
||||
if($value && $this->type !== 'date-duration')
|
||||
{
|
||||
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);
|
||||
}
|
||||
catch(\Exception $e)
|
||||
catch (\Exception $e)
|
||||
{
|
||||
unset($e);
|
||||
$date = null;
|
||||
$value = '';
|
||||
// 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;
|
||||
}
|
||||
elseif ($this->type == 'date-duration')
|
||||
elseif($this->type == 'date-duration')
|
||||
{
|
||||
$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']))
|
||||
{
|
||||
$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
|
||||
$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
|
||||
{
|
||||
@ -201,23 +217,28 @@ class Date extends Transformer
|
||||
}
|
||||
if($date < $min)
|
||||
{
|
||||
self::set_validation_error($form_name,lang(
|
||||
self::set_validation_error($form_name, lang(
|
||||
"Value has to be at least '%1' !!!",
|
||||
$min->format($this->type != 'date')
|
||||
),'');
|
||||
), ''
|
||||
);
|
||||
$value = $min;
|
||||
}
|
||||
}
|
||||
if (!empty($this->attrs['max']) && !empty($value))
|
||||
if(!empty($this->attrs['max']) && !empty($value))
|
||||
{
|
||||
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
|
||||
$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
|
||||
{
|
||||
@ -225,14 +246,15 @@ class Date extends Transformer
|
||||
}
|
||||
if($date > $max)
|
||||
{
|
||||
self::set_validation_error($form_name,lang(
|
||||
self::set_validation_error($form_name, lang(
|
||||
"Value has to be at maximum '%1' !!!",
|
||||
$max->format($this->type != 'date')
|
||||
),'');
|
||||
), ''
|
||||
);
|
||||
$value = $max;
|
||||
}
|
||||
}
|
||||
if ($this->type == 'date-duration')
|
||||
if($this->type == 'date-duration')
|
||||
{
|
||||
$valid = (string)$value === '' ? '' : (int)$value;
|
||||
}
|
||||
@ -241,22 +263,25 @@ class Date extends Transformer
|
||||
// Not null, blank
|
||||
$value = '';
|
||||
}
|
||||
elseif ($date && empty($this->attrs['data_format'])) // integer timestamp
|
||||
elseif($date && empty($this->attrs['data_format'])) // integer timestamp
|
||||
{
|
||||
$valid = $date->format('ts');
|
||||
}
|
||||
// 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
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
\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')
|
||||
);
|
@ -205,29 +205,16 @@ class Template extends Etemplate\Widget
|
||||
/**
|
||||
* 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
|
||||
* @return string url
|
||||
*/
|
||||
public static function rel2url($path)
|
||||
{
|
||||
if ($path)
|
||||
{
|
||||
if ($path[0] === '/')
|
||||
{
|
||||
$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;
|
||||
return $GLOBALS['egw_info']['server']['webserver_url'].'/api/etemplate.php'.
|
||||
($path[0] === '/' ? $path : preg_replace('#^'.self::VFS_TEMPLATE_PATH.'#', '',
|
||||
Api\Vfs::parse_url($path, PHP_URL_PATH))).'?'.filemtime(self::rel2path($path));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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'));
|
||||
|
@ -26,6 +26,7 @@
|
||||
padding: 0px;
|
||||
background-color: white;
|
||||
height: 99%;
|
||||
max-width: 100%
|
||||
}
|
||||
.et2_container > div:not([class]) {
|
||||
height: 100%;
|
||||
@ -315,6 +316,9 @@ button.et2_button_with_image {
|
||||
button.et2_button_with_image.et2_button_text {
|
||||
background-position: 4px center;
|
||||
}
|
||||
button.et2_buttonFitContent, et2-button.et2_buttonFitContent {
|
||||
max-width: fit-content;
|
||||
}
|
||||
/* et2_box_widget ###*/
|
||||
button[id="cancel"],
|
||||
button#cancel {
|
||||
|
@ -85,10 +85,8 @@
|
||||
</rows>
|
||||
</grid>
|
||||
<vbox class="filemanager_config">
|
||||
<button label="Mount /etemplates to allow customizing of eTemplates" id="etemplates"/>
|
||||
<menulist>
|
||||
<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>
|
||||
<button label="Mount /etemplates to allow customizing of eTemplates" id="etemplates" class="et2_buttonFitContent"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<textbox size="30" label="under directory" id="versionedpath" statustext="/ = everywhere"/>
|
||||
|
@ -66,9 +66,11 @@ button.infologExtraButton {
|
||||
button.infologExtraButton:hover {
|
||||
background-size: 16px;
|
||||
}
|
||||
div.et2_hbox.tab_toolbar {
|
||||
|
||||
.tab_toolbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button#infolog-edit_encrypt {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
|
@ -3,7 +3,7 @@
|
||||
<!-- $Id$ -->
|
||||
<overlay>
|
||||
<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"/>
|
||||
</template>
|
||||
<template id="infolog.edit.links" template="" lang="" group="0" version="1.3.001">
|
||||
@ -154,7 +154,7 @@
|
||||
<rows>
|
||||
<row class="dialogHeader">
|
||||
<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"/>
|
||||
<appicon src="infolog" for="info_number"/>
|
||||
</row>
|
||||
@ -165,7 +165,9 @@
|
||||
</menulist>
|
||||
<description/>
|
||||
<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 class="dialogHeader3">
|
||||
<description value="Contact"/>
|
||||
@ -217,32 +219,33 @@
|
||||
</row>
|
||||
<row disabled="!@info_owner" class="dialogOperators">
|
||||
<description value="Owner"/>
|
||||
<hbox width="100%">
|
||||
<menulist>
|
||||
<menupopup type="select-account" id="info_owner" readonly="true"/>
|
||||
</menulist>
|
||||
<et2-hbox>
|
||||
<select-account id="info_owner" readonly="true"/>
|
||||
<date-time id="info_created" readonly="true" align="right"/>
|
||||
</hbox>
|
||||
</et2-hbox>
|
||||
<description/>
|
||||
<description value="Last modified"/>
|
||||
<hbox width="100%">
|
||||
<menulist>
|
||||
<menupopup type="select-account" id="info_modifier" readonly="true"/>
|
||||
</menulist>
|
||||
<et2-hbox>
|
||||
<select-account id="info_modifier" readonly="true"/>
|
||||
<date-time id="info_datemodified" readonly="true" align="right"/>
|
||||
</hbox>
|
||||
</et2-hbox>
|
||||
</row>
|
||||
<row class="dialogFooterToolbar">
|
||||
<hbox span="6">
|
||||
<button statustext="Saves this entry" label="Save" id="button[save]" image="save" background_image="1"/>
|
||||
<button statustext="Apply the changes" label="Apply" id="button[apply]" image="apply" background_image="1"/>
|
||||
<button statustext="leave without saveing the entry" label="Cancel" id="button[cancel]" onclick="window.close();" image="cancel" background_image="1"/>
|
||||
<menulist>
|
||||
<menupopup statustext="Execute a further action for this entry" id="action" onchange="app.infolog.edit_actions()" options="Actions..."/>
|
||||
</menulist>
|
||||
<checkbox label="Do not notify" id="no_notifications" statustext="Do not notify of these changes"/>
|
||||
<hbox span="all">
|
||||
<button statustext="Saves this entry" label="Save" id="button[save]" image="save"
|
||||
background_image="1"/>
|
||||
<button statustext="Apply the changes" label="Apply" id="button[apply]" image="apply"
|
||||
background_image="1"></button>
|
||||
<button statustext="leave without saveing the entry" label="Cancel" id="button[cancel]"
|
||||
onclick="window.close();" image="cancel" background_image="1"/>
|
||||
<select statustext="Execute a further action for this entry" id="action"
|
||||
onchange="app.infolog.edit_actions()" empty_label="Actions..."/>
|
||||
<checkbox label="Do not notify" id="no_notifications"
|
||||
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>
|
||||
<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"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
|
@ -226,7 +226,7 @@
|
||||
* Add / remove link or category popup used for actions on multiple entries
|
||||
*/
|
||||
|
||||
div.action_popup[id] {
|
||||
.action_popup[id] {
|
||||
position: fixed;
|
||||
top: 200px;
|
||||
left: 450px;
|
||||
@ -235,8 +235,8 @@ div.action_popup[id] {
|
||||
display: none;
|
||||
border-collapse:collapse;
|
||||
border-spacing:0px
|
||||
}
|
||||
.action_popup-content {
|
||||
}
|
||||
.action_popup-content {
|
||||
display:block;
|
||||
padding:2ex;
|
||||
color:#666666;
|
||||
|
@ -132,7 +132,7 @@ button.infologExtraButton {
|
||||
button.infologExtraButton:hover {
|
||||
background-size: 16px;
|
||||
}
|
||||
div.et2_hbox.tab_toolbar {
|
||||
.tab_toolbar {
|
||||
position: relative;
|
||||
}
|
||||
button#infolog-edit_encrypt {
|
||||
|
@ -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>
|
@ -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';
|
@ -319,7 +319,7 @@ app.classes.mail = AppJS.extend(
|
||||
{
|
||||
textAreaWidget.tinymce.then(()=>{
|
||||
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
|
||||
});
|
||||
});
|
||||
|
@ -637,7 +637,7 @@ div.mailDisplayHeaders div.mail_extraEmails.visible
|
||||
box-shadow: 5px 5px 5px #aaa;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
div.mailComposeBody {
|
||||
.mailComposeBody {
|
||||
white-space: normal !important;
|
||||
}
|
||||
#mail-compose_mail_plaintext {
|
||||
@ -884,11 +884,11 @@ span#mail-compose_replyto_expander {
|
||||
padding: 0;
|
||||
}
|
||||
/*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;
|
||||
}
|
||||
/*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;
|
||||
}
|
||||
|
||||
|
@ -86,12 +86,12 @@
|
||||
</grid>
|
||||
</vbox>
|
||||
<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"/>
|
||||
</hbox>
|
||||
<hbox disabled="@is_html" class="mailComposeBody mailComposeTextContainer">
|
||||
</old-hbox>
|
||||
<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"/>
|
||||
</hbox>
|
||||
</old-hbox>
|
||||
<vbox class="et2_file mailUploadSection" disabled="@no_griddata">
|
||||
<hbox>
|
||||
<select id="filemode" label="Send files as" onchange="app.mail.check_sharing_filemode"/>
|
||||
|
@ -5,43 +5,43 @@
|
||||
<template id="mail.index.splitter" height="100%" template="" lang="" group="0" version="1.9.001">
|
||||
<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"/>
|
||||
<vbox id="mailPreview" width="100%">
|
||||
<old-vbox id="mailPreview" width="100%">
|
||||
<toolbar id="toolbar"/>
|
||||
<box id="blank" disabled="false">
|
||||
<old-box id="blank" disabled="false">
|
||||
<image src="mail"/>
|
||||
<description value="Select an item to read"/>
|
||||
</box>
|
||||
<hbox width="100%" id="mailPreviewHeadersFrom" class="mailPreviewHeaders">
|
||||
</old-box>
|
||||
<old-hbox width="100%" id="mailPreviewHeadersFrom" class="mailPreviewHeaders">
|
||||
<description value="From"/>
|
||||
<hbox id="additionalFromAddress" class="mail_extraEmails">
|
||||
</hbox>
|
||||
<old-hbox id="additionalFromAddress" class="mail_extraEmails">
|
||||
</old-hbox>
|
||||
<buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/>
|
||||
</hbox>
|
||||
<hbox id="mailPreviewHeadersSubject" class="mailPreviewHeaders">
|
||||
</old-hbox>
|
||||
<old-hbox id="mailPreviewHeadersSubject" class="mailPreviewHeaders">
|
||||
<description value="Subject"/>
|
||||
<description align="left" id="previewSubject" readonly="true" hover_action="app.mail.modifyMessageSubjectDialog" hover_action_title="Modify subject of this message"/>
|
||||
</hbox>
|
||||
<hbox id="mailPreviewHeadersDate" class="mailPreviewHeaders">
|
||||
</old-hbox>
|
||||
<old-hbox id="mailPreviewHeadersDate" class="mailPreviewHeaders">
|
||||
<description value="Date"/>
|
||||
<date-time align="left" id="previewDate" readonly="true"/>
|
||||
</hbox>
|
||||
<hbox width="100%" id="mailPreviewHeadersTo" class="mailPreviewHeaders">
|
||||
</old-hbox>
|
||||
<old-hbox width="100%" id="mailPreviewHeadersTo" class="mailPreviewHeaders">
|
||||
<description value="To"/>
|
||||
<hbox id="additionalToAddress" class="mail_extraEmails">
|
||||
</hbox>
|
||||
<old-hbox id="additionalToAddress" class="mail_extraEmails">
|
||||
</old-hbox>
|
||||
<buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/>
|
||||
</hbox>
|
||||
<hbox id="mailPreviewHeadersCC" class="mailPreviewHeaders">
|
||||
</old-hbox>
|
||||
<old-hbox id="mailPreviewHeadersCC" class="mailPreviewHeaders">
|
||||
<description value="CC"/>
|
||||
<hbox id="additionalCCAddress" class="mail_extraEmails">
|
||||
</hbox>
|
||||
<old-hbox id="additionalCCAddress" class="mail_extraEmails">
|
||||
</old-hbox>
|
||||
<buttononly class="et2_button ui-button" label="Show all Addresses" image="foldertree_nolines_plus" onclick="app.mail.showAllHeader"/>
|
||||
</hbox>
|
||||
<hbox class="mailPreviewHeaders smimeIcons">
|
||||
</old-hbox>
|
||||
<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_encryption" src="smime_encrypt" statustext="Smime encrypted message" disabled="true" align="right" width="24"/>
|
||||
</hbox>
|
||||
<hbox id="mailPreviewHeadersAttachments" class="mailPreviewHeaders">
|
||||
</old-hbox>
|
||||
<old-hbox id="mailPreviewHeadersAttachments" class="mailPreviewHeaders">
|
||||
<description value="Attachments"/>
|
||||
<grid disabled="@no_griddata" id="previewAttachmentArea" class="previewAttachmentArea egwGridView_grid">
|
||||
<columns>
|
||||
@ -72,11 +72,11 @@
|
||||
</rows>
|
||||
</grid>
|
||||
<buttononly class="et2_button ui-button" label="Show all attachments" image="foldertree_nolines_plus" width="16px" height="16px" onclick="app.mail.showAllHeader"/>
|
||||
</hbox>
|
||||
<box id="mailPreviewContainer">
|
||||
</old-hbox>
|
||||
<old-box id="mailPreviewContainer">
|
||||
<iframe frameborder="1" id="messageIFRAME" scrolling="auto"/>
|
||||
</box>
|
||||
</vbox>
|
||||
</old-box>
|
||||
</old-vbox>
|
||||
</split>
|
||||
</template>
|
||||
<template id="mail.index.nosplitter" template="" lang="" group="0" version="1.9.001">
|
||||
|
@ -850,15 +850,15 @@ span#mail-compose_replyto_expander {
|
||||
padding: 0;
|
||||
}
|
||||
/*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,
|
||||
div.mail-compose_toolbar_assist {
|
||||
.mail-compose_toolbar_assist {
|
||||
display: none;
|
||||
}
|
||||
/*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,
|
||||
div.mail-compose_toolbar_assist {
|
||||
.mail-compose_toolbar_assist {
|
||||
display: none;
|
||||
}
|
||||
div.mailComposeHeaderSection > table {
|
||||
@ -2072,6 +2072,7 @@ table#mail-index_previewAttachmentArea::-webkit-scrollbar-thumb:hover {
|
||||
#mail-index_nm.et2_nextmatch .egwGridView_outer thead tr {
|
||||
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;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
8603
package-lock.json
generated
8603
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -6,14 +6,23 @@
|
||||
"repository": {},
|
||||
"scripts": {
|
||||
"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": {
|
||||
"@babel/core": "^7.14.6",
|
||||
"@babel/preset-typescript": "^7.14.5",
|
||||
"@open-wc/testing": "^2.5.33",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@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-contrib-cssmin": "^2.2.1",
|
||||
"grunt-newer": "^1.3.0",
|
||||
@ -21,6 +30,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.52.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sinon": "^11.1.2",
|
||||
"terser": "^4.8.0",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
@ -44,9 +54,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"lit-element": "^2.5.1",
|
||||
"lit-html": "^1.4.1",
|
||||
"sortablejs": "^1.14.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -28,7 +28,19 @@ readdirSync('./chunks').forEach(name => {
|
||||
});
|
||||
|
||||
// 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 = {
|
||||
treeshake: false,
|
||||
@ -65,13 +77,18 @@ const config = {
|
||||
},
|
||||
plugins: [{
|
||||
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)
|
||||
{
|
||||
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 {
|
||||
readFileSync(tsPath);
|
||||
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(),
|
||||
resolve({
|
||||
browser: true
|
||||
}),
|
||||
{
|
||||
transform (code, id) {
|
||||
if (id.endsWith('.ts'))
|
||||
|
75
web-test-runner.config.mjs
Normal file
75
web-test-runner.config.mjs
Normal 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})
|
||||
],
|
||||
};
|
Loading…
Reference in New Issue
Block a user