mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-23 23:29:31 +01:00
merge master into 23.1
This commit is contained in:
commit
9c4d28ca63
@ -58,7 +58,6 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv {
|
||||
*/
|
||||
public function import( $_stream, importexport_definition $_definition ) {
|
||||
parent::import($_stream, $_definition);
|
||||
|
||||
if($_definition->plugin_options['empty_addressbook'])
|
||||
{
|
||||
$this->empty_addressbook($this->user, $this->ids);
|
||||
@ -115,6 +114,45 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv {
|
||||
$contact_owner = $this->user;
|
||||
}
|
||||
$this->user = $contact_owner;
|
||||
|
||||
// Special case fast lookup for simple condition "field exists"
|
||||
// We find ALL matches first to save DB queries. This saves 1 query per row, at the cost of RAM
|
||||
// Should be 10x faster for large (thousands of rows) files, may be slower for small (tens of rows) files
|
||||
$this->cached_condition = [];
|
||||
foreach($definition->plugin_options['conditions'] as $condition)
|
||||
{
|
||||
$contacts = array();
|
||||
$this->cached_condition[$condition['string']] = [];
|
||||
switch($condition['type'])
|
||||
{
|
||||
// exists
|
||||
case 'exists' :
|
||||
$searchcondition = $condition['string'][0] == Api\Storage::CF_PREFIX ? [$condition['string']] : [];
|
||||
|
||||
// if we use account_id for the condition, we need to set the owner for filtering, as this
|
||||
// enables Api\Contacts\Storage to decide what backend is to be used
|
||||
if($condition['string'] == 'account_id')
|
||||
{
|
||||
$searchcondition['owner'] = 0;
|
||||
}
|
||||
$field = $condition['string'][0] == Api\Storage::CF_PREFIX ? 'contact_value' : $condition['string'];
|
||||
$contacts = $this->bocontacts->search(
|
||||
//array( $condition['string'] => $record[$condition['string']],),
|
||||
'',
|
||||
['contact_id', 'cat_id', $field],
|
||||
'', '', '', false, 'AND', false,
|
||||
$searchcondition
|
||||
);
|
||||
foreach($contacts as $contact)
|
||||
{
|
||||
if(!isset($this->cached_condition[$condition['string']][$contact[$field]]))
|
||||
{
|
||||
$this->cached_condition[$condition['string']][$contact[$field]] = [];
|
||||
}
|
||||
$this->cached_condition[$condition['string']][$contact[$field]][] = $contact;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,18 +253,9 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv {
|
||||
switch ( $condition['type'] ) {
|
||||
// exists
|
||||
case 'exists' :
|
||||
if($record_array[$condition['string']]) {
|
||||
$searchcondition = array( $condition['string'] => $record_array[$condition['string']]);
|
||||
// if we use account_id for the condition, we need to set the owner for filtering, as this
|
||||
// enables Api\Contacts\Storage to decide what backend is to be used
|
||||
if ($condition['string']=='account_id') $searchcondition['owner']=0;
|
||||
$contacts = $this->bocontacts->search(
|
||||
//array( $condition['string'] => $record[$condition['string']],),
|
||||
'',
|
||||
$this->definition->plugin_options['update_cats'] == 'add' ? false : true,
|
||||
'', '', '', false, 'AND', false,
|
||||
$searchcondition
|
||||
);
|
||||
if($record_array[$condition['string']] && $this->cached_condition[$condition['string']])
|
||||
{
|
||||
$contacts = $this->cached_condition[$condition['string']][$record_array[$condition['string']]];
|
||||
}
|
||||
if ( is_array( $contacts ) && count( array_keys( $contacts ) ) >= 1 ) {
|
||||
// apply action to all contacts matching this exists condition
|
||||
|
@ -21,18 +21,20 @@ import {fetchAll, nm_action, nm_compare_field} from "../../api/js/etemplate/et2_
|
||||
import "./CRM";
|
||||
import {egw} from "../../api/js/jsapi/egw_global";
|
||||
import {LitElement} from "@lion/core";
|
||||
import {Et2SelectState} from "../../api/js/etemplate/Et2Select/Et2Select";
|
||||
import {Et2SelectCountry} from "../../api/js/etemplate/Et2Select/Et2SelectCountry";
|
||||
import {Et2SelectCountry} from "../../api/js/etemplate/Et2Select/Select/Et2SelectCountry";
|
||||
|
||||
import {Et2SelectState} from "../../api/js/etemplate/Et2Select/Select/Et2SelectState";
|
||||
|
||||
/**
|
||||
* Object to call app.addressbook.openCRMview with
|
||||
*/
|
||||
export interface CrmParams {
|
||||
contact_id: number|string;
|
||||
crm_list?: "infolog"|"tracker"|"infolog-organisation"; // default: use preference
|
||||
title?: string; // default: link-title of contact_id
|
||||
icon?: string; // default: avatar for contact_id
|
||||
index?: number;
|
||||
export interface CrmParams
|
||||
{
|
||||
contact_id : number | string;
|
||||
crm_list? : "infolog" | "tracker" | "infolog-organisation"; // default: use preference
|
||||
title? : string; // default: link-title of contact_id
|
||||
icon? : string; // default: avatar for contact_id
|
||||
index? : number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,36 +3,55 @@
|
||||
* EGroupware - Admin - DB backup and restore
|
||||
*
|
||||
* @link http://www.egroupware.org
|
||||
* @author Ralf Becker <RalfBecker@outdoor-training.de>
|
||||
* @author Ralf Becker <rb@egroupware.org>
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package admin
|
||||
* @version $Id$
|
||||
*/
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Stylite\Vfs\S3;
|
||||
|
||||
class admin_db_backup
|
||||
{
|
||||
var $public_functions = array(
|
||||
/**
|
||||
* @var true[]
|
||||
*/
|
||||
public $public_functions = array(
|
||||
'index' => true,
|
||||
);
|
||||
var $db_backup;
|
||||
/**
|
||||
* @var Api\Db\Backup
|
||||
*/
|
||||
protected $db_backup;
|
||||
|
||||
/**
|
||||
* Method for sheduled backups, called via asynservice
|
||||
* Method for scheduled backups, called via asynservice
|
||||
*/
|
||||
function do_backup()
|
||||
{
|
||||
$this->db_backup = new Api\Db\Backup();
|
||||
if (class_exists(S3\Backup::class) && S3\Backup::available())
|
||||
{
|
||||
$this->db_backup = new S3\Backup();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->db_backup = new Api\Db\Backup();
|
||||
}
|
||||
|
||||
if (($f = $this->db_backup->fopen_backup()))
|
||||
{
|
||||
try {
|
||||
$f = $this->db_backup->fopen_backup();
|
||||
$this->db_backup->backup($f);
|
||||
if(is_resource($f))
|
||||
if (is_resource($f))
|
||||
{
|
||||
fclose($f);
|
||||
}
|
||||
/* Remove old backups. */
|
||||
$this->db_backup->housekeeping();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// log error
|
||||
_egw_log_exception($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,4 +69,4 @@ class admin_db_backup
|
||||
unset($tpl_root, $self);
|
||||
echo $GLOBALS['egw']->framework->footer();
|
||||
}
|
||||
}
|
||||
}
|
@ -16,9 +16,9 @@
|
||||
<rows>
|
||||
<row>
|
||||
<nextmatch-sortheader label="ID" id="token_id"/>
|
||||
<et2-nextmatch-header-account id="account_id" emptyLabel="User" accountType="user">
|
||||
<option value="0">All users</option>
|
||||
</et2-nextmatch-header-account>
|
||||
<et2-nextmatch-header-account id="account_id" emptyLabel="User" accountType="accounts">
|
||||
<option value="0">All users</option>
|
||||
</et2-nextmatch-header-account>
|
||||
<nextmatch-header id="token_apps" label="Applications"/>
|
||||
<nextmatch-sortheader label="Expiration" id="token_valid_until"/>
|
||||
<et2-vbox>
|
||||
|
@ -44,12 +44,47 @@ export class EgwDragActionImplementation implements EgwActionImplementation {
|
||||
const pseudoNumRows = (_selected[0]?._context?._selectionMgr?._selectAll) ?
|
||||
_selected[0]._context?._selectionMgr?._total : _selected.length;
|
||||
|
||||
for (const egwActionObject of _selected) {
|
||||
const row: Node = (egwActionObject.iface.getDOMNode()).cloneNode(true);
|
||||
if (row) {
|
||||
rows.push(row);
|
||||
table.append(row);
|
||||
}
|
||||
// Clone nodes but use copy webComponent properties
|
||||
const carefulClone = (node) =>
|
||||
{
|
||||
// Don't clone text nodes, it causes duplication in et2-description
|
||||
if(node.nodeType == node.TEXT_NODE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let clone = node.cloneNode();
|
||||
|
||||
let widget_class = window.customElements.get(clone.localName);
|
||||
let properties = widget_class ? widget_class.properties : [];
|
||||
for(let key in properties)
|
||||
{
|
||||
clone[key] = node[key];
|
||||
}
|
||||
// Children
|
||||
node.childNodes.forEach(c =>
|
||||
{
|
||||
const child = carefulClone(c)
|
||||
if(child)
|
||||
{
|
||||
clone.appendChild(child);
|
||||
}
|
||||
})
|
||||
if(widget_class)
|
||||
{
|
||||
clone.requestUpdate();
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
for(const egwActionObject of _selected)
|
||||
{
|
||||
const row : Node = carefulClone(egwActionObject.iface.getDOMNode());
|
||||
if(row)
|
||||
{
|
||||
rows.push(row);
|
||||
table.append(row);
|
||||
}
|
||||
index++;
|
||||
if (index == maxRows) {
|
||||
// Label to show number of items
|
||||
@ -195,9 +230,33 @@ export class EgwDragActionImplementation implements EgwActionImplementation {
|
||||
|
||||
event.dataTransfer.setData('application/json', JSON.stringify(data))
|
||||
|
||||
event.dataTransfer.setDragImage(ai.helper, 12, 12);
|
||||
// Wait for any webComponents to finish
|
||||
let wait = [];
|
||||
const webComponents = [];
|
||||
const check = (element) =>
|
||||
{
|
||||
if(typeof element.updateComplete !== "undefined")
|
||||
{
|
||||
webComponents.push(element)
|
||||
element.requestUpdate();
|
||||
wait.push(element.updateComplete);
|
||||
}
|
||||
element.childNodes.forEach(child => check(child));
|
||||
}
|
||||
check(ai.helper);
|
||||
// Clumsily force widget update, since we can't do it async
|
||||
Promise.all(wait).then(() =>
|
||||
{
|
||||
wait = [];
|
||||
webComponents.forEach(e => wait.push(e.updateComplete));
|
||||
Promise.all(wait).then(() =>
|
||||
{
|
||||
event.dataTransfer.setDragImage(ai.helper, 12, 12);
|
||||
debugger;
|
||||
});
|
||||
});
|
||||
|
||||
this.setAttribute('data-egwActionObjID', JSON.stringify(data.selected));
|
||||
this.setAttribute('data-egwActionObjID', JSON.stringify(data.selected));
|
||||
};
|
||||
|
||||
const dragend = (_) => {
|
||||
|
@ -1,4 +1,6 @@
|
||||
import {Directive, directive, html, repeat} from "@lion/core";
|
||||
import {html} from "lit";
|
||||
import {Directive, directive} from "lit/directive.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {et2_activateLinks} from "./et2_core_common";
|
||||
|
||||
/**
|
||||
|
@ -121,6 +121,15 @@ class multi_video extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls load method for all its sub videos
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/load
|
||||
*/
|
||||
load()
|
||||
{
|
||||
this._videos.forEach(_item =>{_item.node.load()});
|
||||
}
|
||||
|
||||
/**
|
||||
* init/update video tags
|
||||
* @param _value
|
||||
@ -130,15 +139,24 @@ class multi_video extends HTMLElement {
|
||||
{
|
||||
let value = _value.split(',');
|
||||
let video = null;
|
||||
let duration = 0;
|
||||
for (let i=0;i<value.length;i++)
|
||||
{
|
||||
video = document.createElement('video');
|
||||
|
||||
if (value[i].match(/&duration=/))
|
||||
{
|
||||
// get duration from url duration param which is necessary for setting duration time of webm file
|
||||
let params = new URLSearchParams(value[i]);
|
||||
duration = parseInt(params.get('duration'));
|
||||
value[i] = value[i].replace(/&duration.*/, '');
|
||||
}
|
||||
video.src = value[i];
|
||||
this._videos[i] = {
|
||||
node:this._wrapper.appendChild(video),
|
||||
loadedmetadata: false,
|
||||
timeupdate: false,
|
||||
duration: 0,
|
||||
duration: duration ? duration : 0,
|
||||
previousDurations: 0,
|
||||
currentTime: 0,
|
||||
active: false,
|
||||
@ -201,7 +219,7 @@ class multi_video extends HTMLElement {
|
||||
});
|
||||
if (allReady) {
|
||||
this._videos.forEach(_item => {
|
||||
_item.duration = _item.node.duration;
|
||||
_item.duration = _item.duration ? _item.duration : _item.node.duration;
|
||||
_item.previousDurations = _item.index > 0 ? this._videos[_item.index-1]['duration'] + this._videos[_item.index-1]['previousDurations'] : 0;
|
||||
});
|
||||
this.duration = this.__duration();
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {css, SlotMixin} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {SlAvatar} from "@shoelace-style/shoelace";
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
@ -18,7 +18,7 @@ import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
||||
import "../../../../vendor/bower-asset/cropper/dist/cropper.min.js";
|
||||
import {cropperStyles} from "./cropperStyles";
|
||||
|
||||
export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDetachedDOM
|
||||
export class Et2Avatar extends Et2Widget(SlAvatar) implements et2_IDetachedDOM
|
||||
{
|
||||
private _contactId;
|
||||
private _delBtn: HTMLElement;
|
||||
@ -91,6 +91,10 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe
|
||||
|
||||
crop: {type: Boolean},
|
||||
|
||||
/**
|
||||
* Explicitly specify the avatar size.
|
||||
* Better to set the --size CSS variable in app.css, since it allows inheritance and overriding
|
||||
*/
|
||||
size: {type: String}
|
||||
}
|
||||
}
|
||||
@ -103,7 +107,6 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe
|
||||
this.contactId = "";
|
||||
this.editable = false;
|
||||
this.crop = false;
|
||||
this.size = "2.7em";
|
||||
this.icon = "";
|
||||
this.shape = "rounded";
|
||||
}
|
||||
@ -246,10 +249,10 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe
|
||||
{
|
||||
let self = this;
|
||||
this._editBtn = document.createElement('et2-button-icon');
|
||||
this._editBtn.setAttribute('name', 'pencil');
|
||||
this._editBtn.setAttribute('image', 'pencil');
|
||||
this._editBtn.setAttribute('part', 'edit');
|
||||
this._delBtn = document.createElement('et2-button-icon');
|
||||
this._delBtn.setAttribute('name', 'trash');
|
||||
this._delBtn.setAttribute('image', 'delete');
|
||||
this._delBtn.setAttribute('part', 'edit');
|
||||
this._baseNode.append(this._editBtn);
|
||||
this._baseNode.append(this._delBtn);
|
||||
@ -434,7 +437,8 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("et2-avatar", Et2Avatar as any);
|
||||
|
||||
customElements.define("et2-avatar", Et2Avatar);
|
||||
// make et2_avatar publicly available as we need to call it from templates
|
||||
{
|
||||
window['et2_avatar'] = Et2Avatar;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {css, html, LitElement, repeat} from "@lion/core";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
|
||||
/**
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
import {Et2Avatar} from "./Et2Avatar";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
|
||||
export class Et2LAvatar extends Et2Avatar
|
||||
{
|
||||
@ -126,4 +126,5 @@ export class Et2LAvatar extends Et2Avatar
|
||||
return {background: bg, initials: text};
|
||||
}
|
||||
}
|
||||
customElements.define("et2-lavatar", Et2LAvatar as any);
|
||||
|
||||
customElements.define("et2-lavatar", Et2LAvatar);
|
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Cropper styles constant
|
||||
*/
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
|
||||
/*!
|
||||
* Cropper.js v1.5.12
|
||||
|
@ -9,9 +9,10 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, LitElement, PropertyValues} from "@lion/core";
|
||||
import {css, LitElement, PropertyValues} from "lit";
|
||||
import '../Et2Image/Et2Image';
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {egw_registerGlobalShortcut} from "../../egw_action/egw_keymanager";
|
||||
|
||||
type Constructor<T = LitElement> = new (...args : any[]) => T;
|
||||
export const ButtonMixin = <T extends Constructor>(superclass : T) => class extends superclass
|
||||
@ -49,6 +50,12 @@ export const ButtonMixin = <T extends Constructor>(superclass : T) => class exte
|
||||
et2_button_delete: /delete(&|\]|$)/ // red
|
||||
};
|
||||
|
||||
static readonly default_keys : object = {
|
||||
//egw_shortcutIdx : id regex
|
||||
_83_C: /save(&|\]|$)/, // CTRL+S
|
||||
_27_: /cancel(&|\]|$)/, // Esc
|
||||
};
|
||||
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
@ -323,6 +330,38 @@ export const ButtonMixin = <T extends Constructor>(superclass : T) => class exte
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* If button ID has a default keyboard shortcut (eg: Save: Ctrl+S), register with egw_keymanager
|
||||
*
|
||||
* @param {string} check_id
|
||||
*/
|
||||
_register_default_keyhandler(check_id : string)
|
||||
{
|
||||
if(!check_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
for(const keyindex in this.constructor.default_keys)
|
||||
{
|
||||
// @ts-ignore
|
||||
if(check_id.match(this.constructor.default_keys[keyindex]))
|
||||
{
|
||||
let [keycode, modifiers] = keyindex.substring(1).split("_");
|
||||
egw_registerGlobalShortcut(
|
||||
parseInt(keycode),
|
||||
modifiers.includes("S"), modifiers.includes("C"), modifiers.includes("A"),
|
||||
() =>
|
||||
{
|
||||
this.dispatchEvent(new MouseEvent("click"));
|
||||
return true;
|
||||
},
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a default class for the button based on ID
|
||||
*
|
||||
|
@ -13,7 +13,7 @@ import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import '../Et2Image/Et2Image';
|
||||
import {SlButton} from "@shoelace-style/shoelace";
|
||||
import {ButtonMixin} from "./ButtonMixin";
|
||||
import {PropertyValues} from "@lion/core";
|
||||
import {PropertyValues} from "lit";
|
||||
|
||||
|
||||
export class Et2Button extends ButtonMixin(Et2InputWidget(SlButton))
|
||||
@ -22,7 +22,7 @@ export class Et2Button extends ButtonMixin(Et2InputWidget(SlButton))
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
label: {type: String}
|
||||
label: {type: String, noAccessor: true}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,9 @@ export class Et2Button extends ButtonMixin(Et2InputWidget(SlButton))
|
||||
{
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
// Register default keyboard shortcut, if applicable
|
||||
this._register_default_keyhandler(this.id);
|
||||
|
||||
if(!this.label && this.__image)
|
||||
{
|
||||
/*
|
||||
|
@ -14,7 +14,7 @@ import '../Et2Image/Et2Image';
|
||||
import {SlIconButton} from "@shoelace-style/shoelace";
|
||||
import {ButtonMixin} from "./ButtonMixin";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
|
||||
|
||||
export class Et2ButtonIcon extends ButtonMixin(Et2InputWidget(SlIconButton))
|
||||
@ -45,7 +45,7 @@ export class Et2ButtonIcon extends ButtonMixin(Et2InputWidget(SlIconButton))
|
||||
}
|
||||
if(new_image && !this.src)
|
||||
{
|
||||
this.name = new_image;
|
||||
this.__name = new_image;
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +53,16 @@ export class Et2ButtonIcon extends ButtonMixin(Et2InputWidget(SlIconButton))
|
||||
{
|
||||
return this.src || this.name;
|
||||
}
|
||||
|
||||
set name(name)
|
||||
{
|
||||
// No - use image to avoid conflicts between our icons & SlIconButton's image/url loading
|
||||
}
|
||||
|
||||
get name()
|
||||
{
|
||||
return super.name;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-button-icon", Et2ButtonIcon);
|
@ -8,7 +8,7 @@
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
import {css, html, LitElement} from "@lion/core";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import {ButtonMixin} from "./ButtonMixin";
|
||||
|
||||
/**
|
||||
@ -87,14 +87,14 @@ export class Et2ButtonScroll extends ButtonMixin(LitElement)
|
||||
<et2-button-icon
|
||||
noSubmit
|
||||
data-direction="1"
|
||||
name="chevron-up"
|
||||
image="chevron-up"
|
||||
part="button"
|
||||
>↑
|
||||
</et2-button-icon>
|
||||
<et2-button-icon
|
||||
noSubmit
|
||||
data-direction="-1"
|
||||
name="chevron-down"
|
||||
image="chevron-down"
|
||||
part="button"
|
||||
>↓
|
||||
</et2-button-icon>
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import '../Et2Image/Et2Image';
|
||||
import {SlCheckbox} from "@shoelace-style/shoelace";
|
||||
|
@ -1,7 +1,8 @@
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
import {et2_checkbox} from "../et2_widget_checkbox";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {classMap, css, html, LitElement} from "@lion/core";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js"
|
||||
import shoelace from "../Styles/shoelace";
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, PropertyValues, render} from "@lion/core";
|
||||
import {css, html, PropertyValues, render} from "lit";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {SlColorPicker} from "@shoelace-style/shoelace";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Sharable date styles constant
|
||||
*/
|
||||
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {colorsDefStyles} from "../Styles/colorsDefStyles";
|
||||
import {cssImage} from "../Et2Widget/Et2Widget";
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html} from "@lion/core";
|
||||
import {css, html} from "lit";
|
||||
import 'lit-flatpickr';
|
||||
import {dateStyles} from "./DateStyles";
|
||||
import type {Instance} from 'flatpickr/dist/types/instance';
|
||||
@ -19,15 +19,11 @@ import flatpickr from "flatpickr";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
import type {HTMLElementWithValue} from "@lion/form-core/types/FormControlMixinTypes";
|
||||
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
|
||||
import {Et2ButtonIcon} from "../Et2Button/Et2ButtonIcon";
|
||||
import {FormControlMixin} from "@lion/form-core";
|
||||
import {LitFlatpickr} from "lit-flatpickr";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
|
||||
const textbox = new Et2Textbox();
|
||||
const button = new Et2ButtonIcon();
|
||||
|
||||
// list of existing localizations from node_modules/flatpicker/dist/l10n directory:
|
||||
const l10n = [
|
||||
'ar', 'at', 'az', 'be', 'bg', 'bn', 'bs', 'cat', 'cs', 'cy', 'da', 'de', 'eo', 'es', 'et', 'fa', 'fi', 'fo',
|
||||
@ -1033,13 +1029,13 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(LitFlatpickr))
|
||||
<div class="et2-date-time__scrollbuttons" part="scrollbuttons" @click=${this.handleScroll}>
|
||||
<et2-button-icon
|
||||
noSubmit
|
||||
name="chevron-up"
|
||||
image="chevron-up"
|
||||
data-direction="1"
|
||||
>↑
|
||||
</et2-button-icon>
|
||||
<et2-button-icon
|
||||
noSubmit
|
||||
name="chevron-down"
|
||||
image="chevron-down"
|
||||
data-direction="-1"
|
||||
>↓
|
||||
</et2-button-icon>
|
||||
|
@ -9,7 +9,8 @@
|
||||
*/
|
||||
|
||||
|
||||
import {classMap, css, html, LitElement} from "@lion/core";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {sprintf} from "../../egw_action/egw_action_common";
|
||||
import {dateStyles} from "./DateStyles";
|
||||
@ -50,7 +51,7 @@ export function formatDuration(value : number | string, options : formatOptions)
|
||||
for(let i = 0; i < options.displayFormat.length; ++i)
|
||||
{
|
||||
let unit = options.displayFormat[i];
|
||||
let val = this._unit_from_value(value, unit, i === 0);
|
||||
let val = this._unit_from_value(value, unit, i === 0, options);
|
||||
if(unit === 's' || unit === 'm' || unit === 'h' && options.displayFormat[0] === 'd')
|
||||
{
|
||||
vals.push(sprintf('%02d', val));
|
||||
@ -132,6 +133,7 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
|
||||
}
|
||||
|
||||
.input-group__after {
|
||||
display: contents;
|
||||
margin-inline-start: var(--sl-input-spacing-medium);
|
||||
}
|
||||
|
||||
@ -274,6 +276,16 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
|
||||
this.formatter = formatDuration;
|
||||
}
|
||||
|
||||
async getUpdateComplete()
|
||||
{
|
||||
const result = await super.getUpdateComplete();
|
||||
|
||||
// Format select does not start with value, needs an update
|
||||
this._formatNode?.requestUpdate("value");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
transformAttributes(attrs)
|
||||
{
|
||||
// Clean formats, but avoid things that need to be expanded like $cont[displayFormat]
|
||||
@ -432,7 +444,10 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
|
||||
for(let i = 0; i < this.displayFormat.length; ++i)
|
||||
{
|
||||
let unit = this.displayFormat[i];
|
||||
let val = this._unit_from_value(_value, unit, i === 0);
|
||||
let val = this._unit_from_value(_value, unit, i === 0, {
|
||||
hoursPerDay: this.hoursPerDay,
|
||||
dataFormat: this.dataFormat
|
||||
});
|
||||
if(unit === 's' || unit === 'm' || unit === 'h' && this.displayFormat[0] === 'd')
|
||||
{
|
||||
vals.push(sprintf('%02d', val));
|
||||
@ -505,9 +520,9 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
|
||||
}
|
||||
}
|
||||
|
||||
private _unit_from_value(_value, _unit, _highest)
|
||||
private _unit_from_value(_value, _unit, _highest, options)
|
||||
{
|
||||
_value *= this._unit2seconds(this.dataFormat);
|
||||
_value *= this._unit2seconds(options.dataFormat);
|
||||
// get value for given _unit
|
||||
switch(_unit)
|
||||
{
|
||||
@ -518,9 +533,9 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
|
||||
return _highest ? _value : _value % 60;
|
||||
case 'h':
|
||||
_value = Math.floor(_value / 3600);
|
||||
return _highest ? _value : _value % this.hoursPerDay;
|
||||
return _highest ? _value : _value % options.hoursPerDay;
|
||||
case 'd':
|
||||
return Math.floor(_value / 3600 / this.hoursPerDay);
|
||||
return Math.floor(_value / 3600 / options.hoursPerDay);
|
||||
}
|
||||
}
|
||||
|
||||
@ -605,15 +620,19 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
|
||||
s: this.shortLabels ? this.egw().lang("s") : this.egw().lang("Seconds")
|
||||
};
|
||||
// It would be nice to use an et2-select here, but something goes weird with the styling
|
||||
const current = this._display.unit || this.displayFormat[0];
|
||||
return html`
|
||||
<et2-select value="${this._display.unit || this.displayFormat[0]}">
|
||||
<sl-select value="${current}">
|
||||
${[...this.displayFormat].map((format : string) =>
|
||||
html`
|
||||
<sl-menu-item value=${format} ?checked=${this._display.unit === format}>
|
||||
<sl-option
|
||||
value=${format}
|
||||
.selected=${(format == current)}
|
||||
>
|
||||
${this.time_formats[format]}
|
||||
</sl-menu-item>`
|
||||
</sl-option>`
|
||||
)}
|
||||
</et2-select>
|
||||
</sl-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -631,7 +650,7 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
|
||||
*/
|
||||
get _formatNode() : HTMLSelectElement
|
||||
{
|
||||
return this.shadowRoot ? this.shadowRoot.querySelector("et2-select") : null;
|
||||
return this.shadowRoot ? this.shadowRoot.querySelector("sl-select") : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html} from "@lion/core";
|
||||
import {css, html} from "lit";
|
||||
import {Et2DateDuration, formatOptions} from "./Et2DateDuration";
|
||||
import {dateStyles} from "./DateStyles";
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {FormControlMixin} from "@lion/form-core";
|
||||
import {classMap, css, html, ifDefined, LitElement, TemplateResult} from "@lion/core";
|
||||
import {css, html, LitElement, TemplateResult} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import {ifDefined} from "lit/directives/if-defined.js";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {dateStyles} from "./DateStyles";
|
||||
import flatpickr from "flatpickr";
|
||||
import {default as rangePlugin} from "flatpickr/dist/plugins/rangePlugin";
|
||||
import {Et2Date, formatDate, parseDate} from "./Et2Date";
|
||||
import {formatDate, parseDate} from "./Et2Date";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {html, LitElement} from "@lion/core";
|
||||
import {html, LitElement} from "lit";
|
||||
import {formatDate, parseDate} from "./Et2Date";
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {html} from "@lion/core";
|
||||
import {html} from "lit";
|
||||
import {parseDate, parseDateTime} from "./Et2Date";
|
||||
import {Et2DateReadonly} from "./Et2DateReadonly";
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {Et2Date, formatDate, formatDateTime} from "./Et2Date";
|
||||
import type {Instance} from "flatpickr/dist/types/instance";
|
||||
import {default as ShortcutButtonsPlugin} from "shortcut-buttons-flatpickr/dist/shortcut-buttons-flatpickr";
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {css, html, LitElement, render} from "@lion/core";
|
||||
import {css, html, LitElement, render} from "lit";
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
import {activateLinks} from "../ActivateLinksDirective";
|
||||
import {et2_csvSplit} from "../et2_core_common";
|
||||
@ -144,14 +144,14 @@ export class Et2Description extends Et2Widget(LitElement) implements et2_IDetach
|
||||
this.requestUpdate('value', oldValue);
|
||||
}
|
||||
|
||||
requestUpdate(attribute, oldValue)
|
||||
updated(changedProperties)
|
||||
{
|
||||
super.requestUpdate(...arguments);
|
||||
super.updated(changedProperties);
|
||||
// Due to how we do the rendering into the light DOM (not sure it's right) we need this after
|
||||
// value change or it won't actually show up
|
||||
if(["value", "href", "activateLinks"].indexOf(attribute) != -1 && this.parentNode)
|
||||
if((changedProperties.has("value") || changedProperties.has("href") || changedProperties.has("activateLinks")) && this.parentNode)
|
||||
{
|
||||
this.updateComplete.then(() => render(this._renderContent(), <HTMLElement><unknown>this));
|
||||
render(this._renderContent(), <HTMLElement><unknown>this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,12 @@
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {et2_button} from "../et2_widget_button";
|
||||
import {et2_widget} from "../et2_core_widget";
|
||||
import {classMap, css, html, ifDefined, LitElement, render, repeat, SlotMixin, styleMap} from "@lion/core";
|
||||
import {css, html, LitElement, render} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import {ifDefined} from "lit/directives/if-defined.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {styleMap} from "lit/directives/style-map.js";
|
||||
import {SlotMixin} from "@lion/core";
|
||||
import {et2_template} from "../et2_widget_template";
|
||||
import {etemplate2} from "../etemplate2";
|
||||
import {egw, IegwAppLocal} from "../../jsapi/egw_global";
|
||||
@ -35,8 +40,6 @@ export interface DialogButton
|
||||
}
|
||||
|
||||
/**
|
||||
* Et2Dialog widget
|
||||
*
|
||||
* A common dialog widget that makes it easy to inform users or prompt for information.
|
||||
*
|
||||
* It is possible to have a custom dialog by using a template, but you can also use
|
||||
@ -47,7 +50,7 @@ export interface DialogButton
|
||||
*
|
||||
* Or a more complete example:
|
||||
* ```js
|
||||
* let callback = function (button_id)
|
||||
* let callback = function (button_id)
|
||||
* {
|
||||
* if(button_id == Et2Dialog.YES_BUTTON)
|
||||
* {
|
||||
@ -78,22 +81,22 @@ export interface DialogButton
|
||||
* ```
|
||||
*
|
||||
* The parameters for the above are all optional, except callback (which can be null) and message:
|
||||
* callback - function called when the dialog closes, or false/null.
|
||||
* - callback - function called when the dialog closes, or false/null.
|
||||
* The ID of the button will be passed. Button ID will be one of the Et2Dialog.*_BUTTON constants.
|
||||
* The callback is _not_ called if the user closes the dialog with the X in the corner, or presses ESC.
|
||||
* message - (plain) text to display
|
||||
* title - Dialog title
|
||||
* value (for prompt)
|
||||
* buttons - Et2Dialog BUTTONS_* constant, or an array of button settings. Use DialogButton interface.
|
||||
* dialog_type - Et2Dialog *_MESSAGE constant
|
||||
* icon - URL of icon
|
||||
* - message - (plain) text to display
|
||||
* - title - Dialog title
|
||||
* - value (for prompt)
|
||||
* - buttons - Et2Dialog BUTTONS_* constant, or an array of button settings. Use DialogButton interface.
|
||||
* - dialog_type - Et2Dialog *_MESSAGE constant
|
||||
* - icon - URL of icon
|
||||
*
|
||||
* Note that these methods will _not_ block program flow while waiting for user input unless you use "await" on getComplete().
|
||||
* The user's input will be provided to the callback.
|
||||
*
|
||||
* You can also create a custom dialog using an etemplate, even setting all the buttons yourself.
|
||||
* ```ts
|
||||
* // Pass egw in the constructor
|
||||
* // Pass egw in the constructor
|
||||
* let dialog = new Et2Dialog(my_egw_reference);
|
||||
*
|
||||
* // Set attributes. They can be set in any way, but this is convenient.
|
||||
@ -131,6 +134,7 @@ export class Et2Dialog extends Et2Widget(SlotMixin(SlDialog))
|
||||
*
|
||||
* @type {IegwAppLocal}
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
protected __egw : IegwAppLocal
|
||||
|
||||
@ -140,6 +144,7 @@ export class Et2Dialog extends Et2Widget(SlotMixin(SlDialog))
|
||||
*
|
||||
* @type {et2_template | null}
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
protected _template_widget : etemplate2 | null;
|
||||
protected _template_promise : Promise<boolean>;
|
||||
@ -148,6 +153,7 @@ export class Et2Dialog extends Et2Widget(SlotMixin(SlDialog))
|
||||
* Treat the dialog as an atomic operation, and use this promise to notify when
|
||||
* "done" instead of (or in addition to) using the callback function.
|
||||
* It gives the button ID and the dialog value.
|
||||
* @internal
|
||||
*/
|
||||
protected _complete_promise : Promise<[number, Object]>;
|
||||
|
||||
@ -166,6 +172,7 @@ export class Et2Dialog extends Et2Widget(SlotMixin(SlDialog))
|
||||
*
|
||||
* @type {number|null}
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
protected _button_id : number | null;
|
||||
|
||||
@ -233,6 +240,10 @@ export class Et2Dialog extends Et2Widget(SlotMixin(SlDialog))
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.dialog_content {
|
||||
height: var(--height, auto);
|
||||
}
|
||||
|
||||
/* Non-modal dialogs don't have an overlay */
|
||||
|
||||
:host(:not([ismodal])) .dialog, :host(:not([isModal])) .dialog__overlay {
|
||||
@ -877,7 +888,7 @@ export class Et2Dialog extends Et2Widget(SlotMixin(SlDialog))
|
||||
}
|
||||
if(this.height)
|
||||
{
|
||||
styles.height = "--height: " + this.height;
|
||||
styles["--height"] = this.height;
|
||||
}
|
||||
|
||||
return html`
|
||||
|
@ -9,11 +9,11 @@
|
||||
*/
|
||||
|
||||
|
||||
import {Et2Button} from "../Et2Button/Et2Button";
|
||||
import {SlButtonGroup, SlDropdown} from "@shoelace-style/shoelace";
|
||||
import {css, html, TemplateResult} from "@lion/core";
|
||||
import {Et2widgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin";
|
||||
import {css, html, LitElement, TemplateResult} from "lit";
|
||||
import {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin";
|
||||
import {SelectOption} from "../Et2Select/FindSelectOptions";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
|
||||
/**
|
||||
* A split button - a button with a dropdown list
|
||||
@ -28,13 +28,14 @@ import {SelectOption} from "../Et2Select/FindSelectOptions";
|
||||
* as for a select box, but the title can also be full HTML if needed.
|
||||
*
|
||||
*/
|
||||
export class Et2DropdownButton extends Et2widgetWithSelectMixin(Et2Button)
|
||||
export class Et2DropdownButton extends Et2WidgetWithSelectMixin(LitElement)
|
||||
{
|
||||
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
...super.styles,
|
||||
shoelace,
|
||||
css`
|
||||
:host {
|
||||
/* Avoid unwanted style overlap from button */
|
||||
@ -98,6 +99,7 @@ export class Et2DropdownButton extends Et2widgetWithSelectMixin(Et2Button)
|
||||
// We have our own render, so we can handle it internally
|
||||
}
|
||||
|
||||
|
||||
render() : TemplateResult
|
||||
{
|
||||
if(this.readonly)
|
||||
|
@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import {Et2DropdownButton} from "../Et2DropdownButton/Et2DropdownButton";
|
||||
import {css, html, PropertyValues, TemplateResult} from "@lion/core";
|
||||
import {css, html, PropertyValues, TemplateResult} from "lit";
|
||||
import {SelectOption} from "../Et2Select/FindSelectOptions";
|
||||
import {et2_INextmatchHeader, et2_nextmatch} from "../et2_extension_nextmatch";
|
||||
import {Et2Image} from "../Et2Image/Et2Image";
|
||||
@ -76,24 +76,24 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
sl-menu-item:hover et2-image[src="trash"] {
|
||||
sl-option:hover et2-image[src="trash"] {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
/* Add star icons - radio button is already in prefix */
|
||||
|
||||
sl-menu-item::part(base) {
|
||||
sl-option::part(base) {
|
||||
background-image: ${cssImage("fav_filter")};
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 16px;
|
||||
background-position: 5px center;
|
||||
}
|
||||
|
||||
sl-menu-item[checked]::part(base) {
|
||||
sl-option[checked]::part(base) {
|
||||
background-image: ${cssImage("favorites")};
|
||||
}
|
||||
|
||||
sl-menu-item:last-child::part(base) {
|
||||
sl-option:last-child::part(base) {
|
||||
background-image: none;
|
||||
}
|
||||
`,
|
||||
@ -185,11 +185,11 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
statustext="${this.egw().lang("Delete")}"></et2-image>`;
|
||||
|
||||
return html`
|
||||
<sl-menu-item value="${option.value}" ?checked="${option.value == this._preferred}">
|
||||
<sl-option value="${option.value}" ?checked="${option.value == this._preferred}">
|
||||
${option.value !== Et2Favorites.ADD_VALUE ? radio : ""}
|
||||
${icon}
|
||||
${option.label}
|
||||
</sl-menu-item>`;
|
||||
</sl-option>`;
|
||||
}
|
||||
|
||||
|
||||
|
@ -9,7 +9,8 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, LitElement, SlotMixin} from "@lion/core";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import {SlotMixin} from "@lion/core";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
|
||||
export class Et2Iframe extends Et2Widget(SlotMixin(LitElement))
|
||||
|
@ -8,8 +8,8 @@
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, LitElement, render, SlotMixin} from "@lion/core";
|
||||
import {css, html, LitElement, render} from "lit";
|
||||
import {SlotMixin} from "@lion/core";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
|
||||
@ -248,4 +248,4 @@ export class Et2Image extends Et2Widget(SlotMixin(LitElement)) implements et2_ID
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-image", Et2Image as any, {extends: 'img'});
|
||||
customElements.define("et2-image", Et2Image, {extends: 'img'});
|
@ -1,10 +1,11 @@
|
||||
import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "../et2_core_interfaces";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {css, dedupeMixin, LitElement, PropertyValues} from "@lion/core";
|
||||
import {css, LitElement, PropertyValues} from "lit";
|
||||
import {Required} from "../Validators/Required";
|
||||
import {ManualMessage} from "../Validators/ManualMessage";
|
||||
import {LionValidationFeedback, Validator} from "@lion/form-core";
|
||||
import {et2_csvSplit} from "../et2_core_common";
|
||||
import {dedupeMixin} from "@lion/core";
|
||||
|
||||
/**
|
||||
* This mixin will allow any LitElement to become an Et2InputWidget
|
||||
@ -486,6 +487,15 @@ const Et2InputWidgetMixin = <T extends Constructor<LitElement>>(superclass : T)
|
||||
{
|
||||
super.transformAttributes(attrs);
|
||||
|
||||
// Set attributes for the form / autofill. It's the individual widget's
|
||||
// responsibility to do something appropriate with these properties.
|
||||
if(this.autocomplete == "on" && window.customElements.get(this.localName).getPropertyOptions("name") != "undefined" &&
|
||||
this.getArrayMgr("content") !== null
|
||||
)
|
||||
{
|
||||
this.name = this.getArrayMgr("content").explodeKey(this.id).pop();
|
||||
}
|
||||
|
||||
// Check whether an validation error entry exists
|
||||
if(this.id && this.getArrayMgr("validation_errors"))
|
||||
{
|
||||
@ -512,7 +522,7 @@ const Et2InputWidgetMixin = <T extends Constructor<LitElement>>(superclass : T)
|
||||
*/
|
||||
async validate(skipManual = false)
|
||||
{
|
||||
if(this.readonly)
|
||||
if(this.readonly || this.disabled)
|
||||
{
|
||||
// Don't validate if the widget is read-only, there's nothing the user can do about it
|
||||
return Promise.resolve();
|
||||
|
@ -82,11 +82,21 @@ export function inputBasicTests(before : Function, test_value : string, value_se
|
||||
{
|
||||
element = await before();
|
||||
});
|
||||
it("no value gives empty string", () =>
|
||||
it("no value gives empty string", async() =>
|
||||
{
|
||||
element.set_value("");
|
||||
await elementUpdated(element);
|
||||
|
||||
// Shows as empty / no value
|
||||
let value = (<Element><unknown>element).querySelector(value_selector) || (<Element><unknown>element).shadowRoot.querySelector(value_selector);
|
||||
assert.isDefined(value, "Bad value selector '" + value_selector + "'");
|
||||
debugger;
|
||||
assert.equal(value.textContent.trim(), "", "Displaying something when there is no value");
|
||||
if(element.multiple)
|
||||
{
|
||||
assert.isEmpty(element.get_value());
|
||||
return;
|
||||
}
|
||||
// Gives no value
|
||||
assert.equal(element.get_value(), "", "Value mismatch");
|
||||
});
|
||||
@ -94,7 +104,7 @@ export function inputBasicTests(before : Function, test_value : string, value_se
|
||||
it("value out matches value in", async() =>
|
||||
{
|
||||
element.set_value(test_value);
|
||||
|
||||
debugger;
|
||||
// wait for asychronous changes to the DOM
|
||||
await elementUpdated(<Element><unknown>element);
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
|
||||
import {ExposeMixin, ExposeValue} from "../Expose/ExposeMixin";
|
||||
import {css, html, LitElement, TemplateResult} from "@lion/core";
|
||||
import {css, html, LitElement, TemplateResult} from "lit";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {css, html, LitElement, PropertyValues} from "lit";
|
||||
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
|
||||
import {css, html, LitElement, PropertyValues, SlotMixin} from "@lion/core";
|
||||
import {SlotMixin} from "@lion/core";
|
||||
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
|
||||
import {LinkInfo} from "./Et2Link";
|
||||
import {Et2Button} from "../Et2Button/Et2Button";
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {cleanSelectOptions, SelectOption} from "../Et2Select/FindSelectOptions";
|
||||
import {css, html, SlotMixin, TemplateResult} from "@lion/core";
|
||||
import {css, html, TemplateResult} from "lit";
|
||||
import {Et2Select} from "../Et2Select/Et2Select";
|
||||
|
||||
|
||||
export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
export class Et2LinkAppSelect extends Et2Select
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
@ -49,22 +49,11 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
}
|
||||
};
|
||||
|
||||
get slots()
|
||||
{
|
||||
return {
|
||||
...super.slots,
|
||||
"": () =>
|
||||
{
|
||||
/*
|
||||
icon.style.width = "var(--icon-width)";
|
||||
icon.style.height = "var(--icon-width)";
|
||||
|
||||
const icon = document.createElement("et2-image");
|
||||
icon.setAttribute("slot", "prefix");
|
||||
icon.setAttribute("src", "api/navbar");
|
||||
icon.style.width = "var(--icon-width)";
|
||||
icon.style.height = "var(--icon-width)";
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
protected __applicationList : string[];
|
||||
protected __onlyApp : string;
|
||||
@ -104,9 +93,6 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
// Set icon
|
||||
this.querySelector(":scope > [slot='prefix']").setAttribute("src", this.egw().link_get_registry(this.value, 'icon') ?? this.value + "/navbar");
|
||||
|
||||
if(!this.value)
|
||||
{
|
||||
// use preference
|
||||
@ -118,7 +104,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
this.value = this.egw().preference('link_app', appname || this.egw().app_name());
|
||||
}
|
||||
// Register to
|
||||
this.addEventListener("change", this._handleChange);
|
||||
this.addEventListener("sl-change", this._handleChange);
|
||||
|
||||
if(this.__onlyApp)
|
||||
{
|
||||
@ -129,7 +115,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
disconnectedCallback()
|
||||
{
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("change", this._handleChange);
|
||||
this.removeEventListener("sl-change", this._handleChange);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,10 +159,9 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
super.value = new_value;
|
||||
}
|
||||
|
||||
_handleChange(e)
|
||||
handleValueChange(e)
|
||||
{
|
||||
// Set icon
|
||||
this.querySelector(":scope > [slot='prefix']").setAttribute("src", this.egw().link_get_registry(this.value, 'icon'));
|
||||
super.handleValueChange(e);
|
||||
|
||||
// update preference
|
||||
let appname = "";
|
||||
@ -198,13 +183,21 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
// Limit to one app
|
||||
if(this.onlyApp)
|
||||
{
|
||||
select_options.push({value: this.onlyApp, label: this.egw().lang(this.onlyApp)});
|
||||
select_options.push({
|
||||
value: this.onlyApp,
|
||||
label: this.egw().lang(this.onlyApp),
|
||||
icon: this.egw().link_get_registry(this.onlyApp, 'icon') ?? this.onlyApp + "/navbar"
|
||||
});
|
||||
}
|
||||
else if(this.applicationList.length > 0)
|
||||
{
|
||||
select_options = this.applicationList.map((app) =>
|
||||
{
|
||||
return {value: app, label: this.egw().lang(app)};
|
||||
return {
|
||||
value: app,
|
||||
label: this.egw().lang(app),
|
||||
icon: this.egw().link_get_registry(app, 'icon') ?? app + "/navbar"
|
||||
};
|
||||
});
|
||||
}
|
||||
else
|
||||
@ -215,29 +208,27 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
{
|
||||
delete select_options['addressbook-email'];
|
||||
}
|
||||
select_options = cleanSelectOptions(select_options);
|
||||
select_options.map((option) =>
|
||||
{
|
||||
option.icon = this.egw().link_get_registry(option.value, 'icon') ?? option.value + "/navbar"
|
||||
});
|
||||
}
|
||||
if (!this.value)
|
||||
{
|
||||
this.value = <string>this.egw().preference('link_app', this.egw().app_name());
|
||||
}
|
||||
this.select_options = cleanSelectOptions(select_options);
|
||||
this.select_options = select_options;
|
||||
}
|
||||
|
||||
|
||||
_optionTemplate(option : SelectOption) : TemplateResult
|
||||
{
|
||||
return html`
|
||||
<sl-menu-item value="${option.value}" title="${option.title}">
|
||||
<sl-option value="${option.value}" title="${option.title}">
|
||||
${this.appIcons ? "" : option.label}
|
||||
${this._iconTemplate(option.value)}
|
||||
</sl-menu-item>`;
|
||||
}
|
||||
|
||||
_iconTemplate(appname)
|
||||
{
|
||||
let url = appname ? this.egw().link_get_registry(appname, 'icon') : "";
|
||||
return html`
|
||||
<et2-image style="width: var(--icon-width)" slot="prefix" src="${url}"></et2-image>`;
|
||||
${this._iconTemplate(option)}
|
||||
</sl-option>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
import {css, html, LitElement, PropertyValues, SlotMixin} from "@lion/core";
|
||||
import {css, html, LitElement, PropertyValues} from "lit";
|
||||
import {SlotMixin} from "@lion/core";
|
||||
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {FormControlMixin} from "@lion/form-core";
|
||||
|
@ -10,8 +10,9 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, repeat, TemplateResult} from "@lion/core";
|
||||
import {Et2Link, LinkInfo} from "./Et2Link";
|
||||
import {css, html, TemplateResult} from "lit";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {LinkInfo} from "./Et2Link";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
import {Et2LinkString} from "./Et2LinkString";
|
||||
import {egwMenu} from "../../egw_action/egw_menu";
|
||||
|
@ -7,7 +7,7 @@
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {Et2Select} from "../Et2Select/Et2Select";
|
||||
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
|
||||
import {Et2Link} from "./Et2Link";
|
||||
@ -86,9 +86,9 @@ export class Et2LinkSearch extends Et2Select
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Set a value we don't have as an option? That's OK, we'll just add it
|
||||
if(changedProperties.has("value") && this.value && (
|
||||
this.menuItems && this.menuItems.length == 0 ||
|
||||
this.menuItems?.filter && this.menuItems.filter(item => this.value.includes(item.value)).length == 0
|
||||
if(changedProperties.has("value") && this.value && this.value.length > 0 && (
|
||||
this.select_options.length == 0 ||
|
||||
this.select_options.filter && this.select_options.filter(item => this.getValueAsArray().includes(item.value)).length == 0
|
||||
))
|
||||
{
|
||||
this._missingOption(this.value)
|
||||
@ -120,19 +120,16 @@ export class Et2LinkSearch extends Et2Select
|
||||
option.label = title || Et2Link.MISSING_TITLE;
|
||||
option.class = "";
|
||||
// It's probably already been rendered, find the item
|
||||
let item = this.menuItems.find(i => i.value === option.value);
|
||||
let item = this.getAllOptions().find(i => i.value === option.value);
|
||||
if(item)
|
||||
{
|
||||
item.textContent = title;
|
||||
item.classList.remove("loading");
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not already rendered, update the select option
|
||||
this.requestUpdate("select_options");
|
||||
// update the displayed text
|
||||
this.updateComplete.then(() => this.syncItemsFromValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -10,7 +10,8 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, LitElement, PropertyValues, render, TemplateResult, until} from "@lion/core";
|
||||
import {css, html, LitElement, PropertyValues, render, TemplateResult} from "lit";
|
||||
import {until} from "lit/directives/until.js";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {Et2Link, LinkInfo} from "./Et2Link";
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
|
@ -12,7 +12,8 @@
|
||||
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
|
||||
import {css, html, LitElement, ScopedElementsMixin} from "@lion/core";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import {ScopedElementsMixin} from "@lion/core";
|
||||
import {et2_createWidget, et2_widget} from "../et2_core_widget";
|
||||
import {et2_file} from "../et2_widget_file";
|
||||
import {Et2Button} from "../Et2Button/Et2Button";
|
||||
@ -140,7 +141,7 @@ export class Et2LinkTo extends Et2InputWidget(ScopedElementsMixin(FormControlMix
|
||||
<et2-link-entry .onlyApp="${this.onlyApp}"
|
||||
.applicationList="${this.applicationList}"
|
||||
.readonly=${this.readonly}
|
||||
@sl-select=${this.handleEntrySelected}
|
||||
@sl-change=${this.handleEntrySelected}
|
||||
@sl-clear="${this.handleEntryCleared}">
|
||||
</et2-link-entry>
|
||||
<et2-button id="link_button" label="Link" class="link" .noSubmit=${true}
|
||||
@ -357,6 +358,8 @@ export class Et2LinkTo extends Et2InputWidget(ScopedElementsMixin(FormControlMix
|
||||
|
||||
// Clear link entry
|
||||
this.select.value = {app: this.select.app, id: ""};
|
||||
this.select._searchNode.clearSearch();
|
||||
this.select._searchNode.select_options = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -404,6 +407,7 @@ export class Et2LinkTo extends Et2InputWidget(ScopedElementsMixin(FormControlMix
|
||||
if(event.target == this.select._searchNode)
|
||||
{
|
||||
this.classList.add("can_link");
|
||||
this.link_button.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
/**
|
||||
* Column selector for nextmatch
|
||||
*/
|
||||
import {classMap, css, html, LitElement, repeat, TemplateResult} from "@lion/core";
|
||||
import {css, html, LitElement, TemplateResult} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {et2_nextmatch_customfields} from "../et2_extension_nextmatch";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
@ -43,6 +45,10 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
|
||||
background-repeat: no-repeat;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
sl-menu-item::part(label), sl-menu-item::part(submenu-icon) {
|
||||
cursor: initial;
|
||||
}
|
||||
/* Change vertical alignment of CF checkbox line to up with title, not middle */
|
||||
.custom_fields::part(base) {
|
||||
align-items: baseline;
|
||||
@ -73,14 +79,12 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
|
||||
{
|
||||
super(...args);
|
||||
|
||||
this.columnClickHandler = this.columnClickHandler.bind(this);
|
||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
this.sort = Sortable.create(this.shadowRoot.querySelector('sl-menu'), {
|
||||
@ -99,7 +103,7 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
|
||||
<sl-icon slot="header" name="check-all" @click=${this.handleSelectAll}
|
||||
title="${this.egw().lang("Select all")}"
|
||||
style="font-size:24px"></sl-icon>
|
||||
<sl-menu @sl-select="${this.columnClickHandler}" part="columns" slot="content">
|
||||
<sl-menu part="columns" slot="content">
|
||||
${repeat(this.__columns, (column) => column.id, (column) => this.rowTemplate(column))}
|
||||
</sl-menu>`;
|
||||
}
|
||||
@ -132,13 +136,20 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
|
||||
*/
|
||||
protected rowTemplate(column) : TemplateResult
|
||||
{
|
||||
let isCustom = column.widget?.instanceOf(et2_nextmatch_customfields) || false;
|
||||
/* ?disabled=${column.visibility == et2_dataview_column.ET2_COL_VISIBILITY_DISABLED} */
|
||||
const isCustom = column.widget?.instanceOf(et2_nextmatch_customfields) || false;
|
||||
const alwaysOn = [et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS, et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT].indexOf(column.visibility) !== -1;
|
||||
|
||||
// Don't show disabled columns
|
||||
if(column.visibility == et2_dataview_column.ET2_COL_VISIBILITY_DISABLED)
|
||||
{
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<sl-menu-item
|
||||
value="${column.id}"
|
||||
?checked=${column.visibility == et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE}
|
||||
|
||||
value="${column.id.replaceAll(" ", "___")}"
|
||||
type="checkbox"
|
||||
?checked=${alwaysOn || column.visibility == et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE}
|
||||
?disabled=${alwaysOn}
|
||||
title="${column.title}"
|
||||
class="${classMap({
|
||||
select_row: true,
|
||||
@ -182,14 +193,6 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
|
||||
<sl-divider></sl-divider>`;
|
||||
}
|
||||
|
||||
columnClickHandler(event)
|
||||
{
|
||||
const item = event.detail.item;
|
||||
|
||||
// Toggle checked state
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
|
||||
handleSelectAll(event)
|
||||
{
|
||||
let checked = (<SlMenuItem>this.shadowRoot.querySelector("sl-menu-item")).checked || false;
|
||||
@ -220,7 +223,7 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
|
||||
{
|
||||
menuItem.querySelectorAll("[value][checked]").forEach((cf : SlMenuItem) =>
|
||||
{
|
||||
value.push(cf.value);
|
||||
value.push(cf.value.replaceAll("___", " "));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Et2SelectAccount} from "../../Et2Select/Et2SelectAccount";
|
||||
import {Et2SelectAccount} from "../../Et2Select/Select/Et2SelectAccount";
|
||||
import {et2_INextmatchHeader} from "../../et2_extension_nextmatch";
|
||||
import {FilterMixin} from "./FilterMixin";
|
||||
|
||||
|
@ -2,7 +2,8 @@ import {loadWebComponent} from "../../Et2Widget/Et2Widget";
|
||||
import {Et2Select} from "../../Et2Select/Et2Select";
|
||||
import {Et2InputWidget, Et2InputWidgetInterface} from "../../Et2InputWidget/Et2InputWidget";
|
||||
import {FilterMixin} from "./FilterMixin";
|
||||
import {html, LitElement} from "@lion/core";
|
||||
import {html, LitElement} from "lit";
|
||||
import {cleanSelectOptions} from "../../Et2Select/FindSelectOptions";
|
||||
|
||||
/**
|
||||
* Filter by some other type of widget
|
||||
@ -84,6 +85,20 @@ export class Et2CustomFilterHeader extends FilterMixin(Et2InputWidget(LitElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New filter options from server
|
||||
* @param new_options
|
||||
*/
|
||||
set_select_options(new_options)
|
||||
{
|
||||
const widget_class = window.customElements.get(this.filter_node?.localName);
|
||||
const property = widget_class.getPropertyOptions('select_options');
|
||||
if(this.filter_node && property)
|
||||
{
|
||||
this.filter_node.select_options = cleanSelectOptions(new_options);
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return html`
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {egw} from "../../../jsapi/egw_global";
|
||||
import {et2_INextmatchHeader, et2_nextmatch} from "../../et2_extension_nextmatch";
|
||||
import {LitElement} from "@lion/core";
|
||||
import {LitElement} from "lit";
|
||||
|
||||
// Export the Interface for TypeScript
|
||||
type Constructor<T = LitElement> = new (...args : any[]) => T;
|
||||
|
@ -15,8 +15,9 @@ import {SlCard} from "@shoelace-style/shoelace";
|
||||
import interact from "@interactjs/interactjs";
|
||||
import type {InteractEvent} from "@interactjs/core/InteractEvent";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
import {classMap, css, html, TemplateResult} from "@lion/core";
|
||||
import {HasSlotController} from "@shoelace-style/shoelace/dist/internal/slot";
|
||||
import {css, html, TemplateResult} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import type {HasSlotController} from "../../../../node_modules/@shoelace-style/shoelace/dist/internal/slot";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
||||
import {et2_IResizeable} from "../et2_core_interfaces";
|
||||
@ -92,11 +93,11 @@ export class Et2Portlet extends Et2Widget(SlCard)
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.portlet__header et2-button-icon {
|
||||
.portlet__header .portlet__settings-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portlet__header:hover et2-button-icon {
|
||||
.portlet__header:hover .portlet__settings-icon {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@ -577,8 +578,9 @@ export class Et2Portlet extends Et2Widget(SlCard)
|
||||
|
||||
<header class="portlet__header">
|
||||
<slot name="header" part="header" class="card__header">${this.headerTemplate()}</slot>
|
||||
<et2-button-icon id="settings" name="gear" label="Settings" noSubmit=true
|
||||
@click="${() => this.edit_settings()}"></et2-button-icon>
|
||||
<sl-icon-button id="settings" name="gear" label="Settings" class="portlet__settings-icon"
|
||||
@click="${() => this.edit_settings()}">
|
||||
</sl-icon-button>
|
||||
</header>
|
||||
<slot part="body" class="card__body">${this.bodyTemplate()}</slot>
|
||||
<slot name="footer" part="footer" class="card__footer">${this.footerTemplate()}</slot>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {SlMenu} from "@shoelace-style/shoelace";
|
||||
import {Et2widgetWithSelectMixin} from "./Et2WidgetWithSelectMixin";
|
||||
import {Et2WidgetWithSelectMixin} from "./Et2WidgetWithSelectMixin";
|
||||
import {RowLimitedMixin} from "../Layout/RowLimitedMixin";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {css, html, TemplateResult} from "@lion/core";
|
||||
import {css, html, TemplateResult} from "lit";
|
||||
import {SelectOption} from "./FindSelectOptions";
|
||||
|
||||
/**
|
||||
@ -12,7 +12,7 @@ import {SelectOption} from "./FindSelectOptions";
|
||||
*
|
||||
* Use Et2Selectbox in most cases, it's better.
|
||||
*/
|
||||
export class Et2Listbox extends RowLimitedMixin(Et2widgetWithSelectMixin(SlMenu))
|
||||
export class Et2Listbox extends RowLimitedMixin(Et2WidgetWithSelectMixin(SlMenu))
|
||||
{
|
||||
|
||||
static get styles()
|
||||
@ -40,7 +40,8 @@ export class Et2Listbox extends RowLimitedMixin(Et2widgetWithSelectMixin(SlMenu)
|
||||
overflow-x: clip;
|
||||
}
|
||||
/* Ellipsis when too small */
|
||||
sl-menu-item.menu-item__label {
|
||||
|
||||
sl-option.option__label {
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
/* This is usually not used due to flex, but is the basis for ellipsis calculation */
|
||||
@ -153,15 +154,15 @@ export class Et2Listbox extends RowLimitedMixin(Et2widgetWithSelectMixin(SlMenu)
|
||||
// Tag used must match this.optionTag, but you can't use the variable directly.
|
||||
// Pass option along so SearchMixin can grab it if needed
|
||||
return html`
|
||||
<sl-menu-item
|
||||
<sl-option
|
||||
value="${option.value}"
|
||||
title="${!option.title || this.noLang ? option.title : this.egw().lang(option.title)}"
|
||||
class="${option.class}" .option=${option}
|
||||
?checked=${checked}
|
||||
.selected=${checked}
|
||||
>
|
||||
${icon}
|
||||
${this.noLang ? option.label : this.egw().lang(option.label)}
|
||||
</sl-menu-item>`;
|
||||
</sl-option>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,177 +0,0 @@
|
||||
/**
|
||||
* EGroupware eTemplate2 - Select Category WebComponent
|
||||
*
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package api
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
|
||||
import {css, PropertyValues} from "@lion/core";
|
||||
import {Et2Select} from "./Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions as so} from "./StaticOptions";
|
||||
import {cleanSelectOptions} from "./FindSelectOptions";
|
||||
|
||||
/**
|
||||
* Customised Select widget for categories
|
||||
* This widget gives us category colors and icons in the options and selected value.
|
||||
*/
|
||||
export class Et2SelectCategory extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
...super.styles,
|
||||
css`
|
||||
/* Category color on options */
|
||||
::slotted(*) {
|
||||
border-left: 6px solid var(--category-color, transparent);
|
||||
}
|
||||
/* Border on the (single) selected value */
|
||||
:host(.hasValue:not([multiple])) .select--standard .select__control {
|
||||
border-left: 6px solid var(--sl-input-border-color);
|
||||
}
|
||||
`
|
||||
]
|
||||
}
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
/**
|
||||
* Include global categories
|
||||
*/
|
||||
globalCategories: {type: Boolean},
|
||||
/**
|
||||
* Show categories from this application. If not set, will be the current application
|
||||
*/
|
||||
application: {type: String},
|
||||
/**
|
||||
* Show categories below this parent category
|
||||
*/
|
||||
parentCat: {type: Number}
|
||||
}
|
||||
}
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
// we should not translate categories name
|
||||
this.noLang = true;
|
||||
}
|
||||
|
||||
async connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
if(typeof this.application == 'undefined')
|
||||
{
|
||||
this.application =
|
||||
// When the widget is first created, it doesn't have a parent and can't find it's instanceManager
|
||||
(this.getInstanceManager() && this.getInstanceManager().app) ||
|
||||
this.egw().app_name();
|
||||
}
|
||||
// If app passes options (addressbook index) we'll use those instead.
|
||||
// They will be found automatically by update() after ID is set.
|
||||
await this.updateComplete;
|
||||
if(this.select_options.length == 0)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
willUpdate(changedProperties : PropertyValues)
|
||||
{
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if(changedProperties.has("global_categories") || changedProperties.has("application") || changedProperties.has("parentCat"))
|
||||
{
|
||||
this.fetchComplete = so.cat(this).then(options =>
|
||||
{
|
||||
this.static_options = cleanSelectOptions(options);
|
||||
this.requestUpdate("select_options");
|
||||
});
|
||||
}
|
||||
|
||||
if(changedProperties.has("value") || changedProperties.has('select_options'))
|
||||
{
|
||||
this.doLabelChange()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override from parent (SlSelect) to customise display of the current value.
|
||||
* Here's where we add the icon & color border
|
||||
*/
|
||||
doLabelChange()
|
||||
{
|
||||
// Update the display label when checked menu item's label changes
|
||||
if(this.multiple)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const checkedItem = this.menuItems.find(item => item.value === this.value);
|
||||
this.displayLabel = checkedItem ? checkedItem.textContent : '';
|
||||
this.querySelector("[slot=prefix].tag_image")?.remove();
|
||||
if(checkedItem)
|
||||
{
|
||||
let image = this._createImage(checkedItem)
|
||||
if(image)
|
||||
{
|
||||
this.append(image);
|
||||
}
|
||||
this.dropdown.querySelector(".select__control").style.borderColor =
|
||||
getComputedStyle(checkedItem).getPropertyValue("--category-color") || "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render select_options as child DOM Nodes
|
||||
*
|
||||
* Overridden here so we can re-do the displayed label after first load of select options.
|
||||
* Initial load order / lifecycle does not have all the options at the right time
|
||||
* @protected
|
||||
*/
|
||||
protected _renderOptions()
|
||||
{
|
||||
// @ts-ignore Doesn't know about Et2WidgetWithSelectMixin._renderOptions()
|
||||
return super._renderOptions().then(() =>
|
||||
{
|
||||
// @ts-ignore Doesn't know about SlSelect.menuItems
|
||||
if(this.menuItems.length > 0)
|
||||
{
|
||||
this.doLabelChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a custom tag for when multiple=true
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get tagTag() : string
|
||||
{
|
||||
return "et2-category-tag";
|
||||
}
|
||||
|
||||
/**
|
||||
* Customise how tags are rendered.
|
||||
* This overrides parent to set application
|
||||
*
|
||||
* @param item
|
||||
* @protected
|
||||
*/
|
||||
protected _createTagNode(item)
|
||||
{
|
||||
let tag = super._createTagNode(item);
|
||||
tag.application = this.application;
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-cat", Et2SelectCategory);
|
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* EGroupware eTemplate2 - Select Country WebComponent
|
||||
*
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package api
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
|
||||
import {Et2Select} from "./Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions as so} from "./StaticOptions";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
import {SelectOption} from "./FindSelectOptions";
|
||||
|
||||
/**
|
||||
* Customised Select widget for countries
|
||||
* This widget uses CSS from api/templates/default/css/flags.css to set flags
|
||||
*/
|
||||
egw(window).includeCSS("api/templates/default/css/flags.css")
|
||||
|
||||
export class Et2SelectCountry extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
/* Reflect the value so we can use CSS selectors */
|
||||
value: {type: String, reflect: true}
|
||||
}
|
||||
}
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.search = true;
|
||||
|
||||
(<Promise<SelectOption[]>>so.country(this, {}, true)).then(options =>
|
||||
{
|
||||
this.static_options = options
|
||||
this.requestUpdate("select_options");
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
// Add element for current value flag
|
||||
this.querySelector("[slot=prefix].tag_image")?.remove();
|
||||
let image = document.createElement("span");
|
||||
image.slot = "prefix";
|
||||
image.classList.add("tag_image", "flag");
|
||||
this.appendChild(image);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-country", Et2SelectCountry);
|
@ -8,7 +8,8 @@
|
||||
*/
|
||||
|
||||
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {html, LitElement, PropertyValues, render, TemplateResult} from "@lion/core";
|
||||
import {html, LitElement, PropertyValues, render, TemplateResult} from "lit";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {et2_readAttrWithDefault} from "../et2_core_xml";
|
||||
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
|
||||
import {SearchMixinInterface} from "./SearchMixin";
|
||||
@ -17,7 +18,7 @@ import {SearchMixinInterface} from "./SearchMixin";
|
||||
* Base class for things that do selectbox type behaviour, to avoid putting too much or copying into read-only
|
||||
* selectboxes, also for common handling of properties for more special selectboxes.
|
||||
*
|
||||
* As with most other widgets that extend Lion components, do not override render().
|
||||
* As with most other widgets that extend Shoelace components, do not override render() without good reason.
|
||||
* To extend this mixin, override:
|
||||
* - _optionTargetNode(): Return the HTMLElement where the "options" go.
|
||||
* - _optionTemplate(option:SelectOption): Renders the option. To use a special widget, use its tag in render.
|
||||
@ -46,46 +47,54 @@ import {SearchMixinInterface} from "./SearchMixin";
|
||||
* You can specify something else, or return {} to do your own thing. This is a little more complicated. You should
|
||||
* also override _inputGroupInputTemplate() to do what you normally would in render().
|
||||
*
|
||||
*
|
||||
* Technical note:
|
||||
* LionSelect (and any other LionField) use slots to wrap a real DOM node. ET2 doesn't expect this,
|
||||
* so we have to create the input node (via slots()) and respect that it is _external_ to the Web Component.
|
||||
* This complicates things like adding the options, since we can't just override _inputGroupInputTemplate()
|
||||
* and include them when rendering - the parent expects to find the <select> added via a slot, render() would
|
||||
* put it inside the shadowDOM. That's fine, but then it doesn't get created until render(), and the parent
|
||||
* (LionField) can't find it when it looks for it before then.
|
||||
*
|
||||
*/
|
||||
// Export the Interface for TypeScript
|
||||
type Constructor<T = {}> = new (...args : any[]) => T;
|
||||
|
||||
export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(superclass : T) =>
|
||||
export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(superclass : T) =>
|
||||
{
|
||||
class Et2WidgetWithSelect extends Et2InputWidget(superclass)
|
||||
{
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
/**
|
||||
* Textual label for first row, eg: 'All' or 'None'. It's value will be ''
|
||||
*/
|
||||
emptyLabel: String,
|
||||
|
||||
/**
|
||||
* Select box options
|
||||
*
|
||||
* Will be found automatically based on ID and type, or can be set explicitly in the template using
|
||||
* <option/> children, or using widget.select_options = SelectOption[]
|
||||
*/
|
||||
select_options: {type: Object, noAccessor: true},
|
||||
|
||||
/**
|
||||
* Limit size
|
||||
*/
|
||||
rows: {type: Number, noAccessor: true, reflect: true}
|
||||
/**
|
||||
* The current value of the select, submitted as a name/value pair with form data. When `multiple` is enabled, the
|
||||
* value attribute will be a space-delimited list of values based on the options selected, and the value property will
|
||||
* be an array.
|
||||
*
|
||||
@property({
|
||||
noAccessor: true,
|
||||
converter: {
|
||||
fromAttribute: (value : string) => value.split(',')
|
||||
}
|
||||
}
|
||||
})
|
||||
value : string | string[] = "";
|
||||
*/
|
||||
|
||||
/**
|
||||
* Textual label for first row, eg: 'All' or 'None'. It's value will be ''
|
||||
*/
|
||||
@property({type: String})
|
||||
emptyLabel : String = "";
|
||||
|
||||
/**
|
||||
* Limit size
|
||||
*/
|
||||
@property({type: Number, noAccessor: true, reflect: true})
|
||||
|
||||
|
||||
/**
|
||||
* Internal list of possible select options
|
||||
*
|
||||
* This is where we keep options sent from the server. This is not always the complete list, as extending
|
||||
* classes may have their own options to add in. For example, static options are kept separate, as are search
|
||||
* results. The select_options getter should give the complete list.
|
||||
*/
|
||||
private __select_options : SelectOption[] = [];
|
||||
|
||||
/**
|
||||
* When we create the select option elements, it takes a while.
|
||||
* If we don't wait for them, it causes issues in SlSelect
|
||||
*/
|
||||
protected _optionRenderPromise : Promise<void> = Promise.resolve();
|
||||
|
||||
/**
|
||||
* Options found in the XML when reading the template
|
||||
@ -101,6 +110,13 @@ export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
|
||||
this.__select_options = <SelectOption[]>[];
|
||||
}
|
||||
|
||||
async getUpdateComplete() : Promise<boolean>
|
||||
{
|
||||
const result = await super.getUpdateComplete();
|
||||
await this._optionRenderPromise;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @param {import('@lion/core').PropertyValues } changedProperties */
|
||||
updated(changedProperties : PropertyValues)
|
||||
{
|
||||
@ -116,26 +132,48 @@ export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
willUpdate(changedProperties : PropertyValues<this>)
|
||||
{
|
||||
// Add in actual option tags to the DOM based on the new select_options
|
||||
if(changedProperties.has('select_options') || changedProperties.has("emptyLabel"))
|
||||
{
|
||||
// Add in options as children to the target node
|
||||
this._renderOptions();
|
||||
const optionPromise = this._renderOptions();
|
||||
|
||||
// This is needed to display initial load value in some cases, like infolog nm header filters
|
||||
if(this.handleMenuSlotChange && !this.hasUpdated)
|
||||
if(typeof this.selectionChanged !== "undefined")
|
||||
{
|
||||
this.handleMenuSlotChange();
|
||||
optionPromise.then(async() =>
|
||||
{
|
||||
await this.updateComplete;
|
||||
this.selectionChanged();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getValueAsArray()
|
||||
{
|
||||
if(Array.isArray(this.value))
|
||||
{
|
||||
return this.value;
|
||||
}
|
||||
if(this.value == "null" || this.value == null || typeof this.value == "undefined" || !this.emptyLabel && this.value == "")
|
||||
{
|
||||
return [];
|
||||
}
|
||||
return [this.value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render select_options as child DOM Nodes
|
||||
* @protected
|
||||
*/
|
||||
protected _renderOptions()
|
||||
{
|
||||
return Promise.resolve();
|
||||
// Add in options as children to the target node
|
||||
if(!this._optionTargetNode)
|
||||
{
|
||||
@ -156,7 +194,7 @@ export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
|
||||
.map(this._groupTemplate.bind(this))}`;
|
||||
|
||||
render(options, temp_target);
|
||||
return Promise.all(([...temp_target.querySelectorAll(":scope > *")].map(item => item.render)))
|
||||
this._optionRenderPromise = Promise.all(([...temp_target.querySelectorAll(":scope > *")].map(item => item.render)))
|
||||
.then(() =>
|
||||
{
|
||||
this._optionTargetNode.replaceChildren(
|
||||
@ -168,23 +206,7 @@ export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
|
||||
this.handleMenuSlotChange();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwritten as sometimes called before this._inputNode is available
|
||||
*
|
||||
* @param {*} v - modelValue: can be an Object, Number, String depending on the
|
||||
* input type(date, number, email etc)
|
||||
* @returns {string} formattedValue
|
||||
*/
|
||||
formatter(v)
|
||||
{
|
||||
if (!this._inputNode)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
return super.formatter(v);
|
||||
return this._optionRenderPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -212,6 +234,13 @@ export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
|
||||
this.select_options = <SelectOption[]>new_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select box options
|
||||
*
|
||||
* Will be found automatically based on ID and type, or can be set explicitly in the template using
|
||||
* <option/> children, or using widget.select_options = SelectOption[]
|
||||
*/
|
||||
@property({type: Object})
|
||||
get select_options() : SelectOption[]
|
||||
{
|
||||
return this.__select_options;
|
||||
@ -262,7 +291,7 @@ export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
|
||||
* @param {SelectOption} option
|
||||
* @returns {TemplateResult}
|
||||
*/
|
||||
_optionTemplate(option : SelectOption) : TemplateResult
|
||||
protected _optionTemplate(option : SelectOption) : TemplateResult
|
||||
{
|
||||
return html`
|
||||
<span>Override _optionTemplate(). ${option.value} => ${option.label}</span>`;
|
||||
@ -276,7 +305,7 @@ export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
|
||||
}
|
||||
return html`
|
||||
|
||||
<sl-menu-label>${this.noLang ? option.label : this.egw().lang(option.label)}</sl-menu-label>
|
||||
<small>${this.noLang ? option.label : this.egw().lang(option.label)}</small>
|
||||
${option.value.map(this._optionTemplate.bind(this))}
|
||||
<sl-divider></sl-divider>
|
||||
`;
|
||||
|
@ -18,6 +18,9 @@ export interface SelectOption
|
||||
// Show the option, but it is not selectable.
|
||||
// If multiple=true and the option is in the value, it is not removable.
|
||||
disabled? : boolean;
|
||||
// If a search is in progress, does this option match.
|
||||
// Automatically changed.
|
||||
isMatch? : boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,13 +7,13 @@
|
||||
* @author Ralf Becker <rb@egroupware.org>
|
||||
*/
|
||||
|
||||
import {Et2Select} from "./Et2Select";
|
||||
import {cleanSelectOptions, SelectOption} from "./FindSelectOptions";
|
||||
import {SelectAccountMixin} from "./SelectAccountMixin";
|
||||
import {Et2StaticSelectMixin} from "./StaticOptions";
|
||||
import {html, nothing} from "@lion/core";
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {cleanSelectOptions, SelectOption} from "../FindSelectOptions";
|
||||
import {SelectAccountMixin} from "../SelectAccountMixin";
|
||||
import {Et2StaticSelectMixin} from "../StaticOptions";
|
||||
import {html, nothing} from "lit";
|
||||
|
||||
export type AccountType = 'accounts'|'groups'|'both'|'owngroups';
|
||||
export type AccountType = 'accounts' | 'groups' | 'both' | 'owngroups';
|
||||
|
||||
/**
|
||||
* @customElement et2-select-account
|
||||
@ -51,32 +51,47 @@ export class Et2SelectAccount extends SelectAccountMixin(Et2StaticSelectMixin(Et
|
||||
super.connectedCallback();
|
||||
|
||||
// Start fetch of select_options
|
||||
this.fetchComplete = this._getAccounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fill the account list according to type & preferences
|
||||
*
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
protected _getAccounts()
|
||||
{
|
||||
const type = this.egw().preference('account_selection', 'common');
|
||||
let fetch = [];
|
||||
let process = (options) =>
|
||||
{
|
||||
// Shallow copy to avoid re-using the same object.
|
||||
// Uses more memory, but otherwise multiple selectboxes get "tied" together
|
||||
let cleaned = cleanSelectOptions(options)
|
||||
// slice to avoid problems with lots of accounts
|
||||
.slice(0, /* Et2WidgetWithSearch.RESULT_LIMIT */ 100);
|
||||
this.account_options = this.account_options.concat(cleaned);
|
||||
};
|
||||
// for primary_group we only display owngroups == own memberships, not other groups
|
||||
if(type === 'primary_group' && this.accountType !== 'accounts')
|
||||
{
|
||||
if(this.accountType === 'both')
|
||||
{
|
||||
fetch.push(this.egw().accounts('accounts').then(options => {this.static_options = this.static_options.concat(cleanSelectOptions(options))}));
|
||||
fetch.push(this.egw().accounts('accounts').then(process));
|
||||
}
|
||||
|
||||
fetch.push(this.egw().accounts('owngroups').then(options => {this.static_options = this.static_options.concat(cleanSelectOptions(options))}));
|
||||
fetch.push(this.egw().accounts('owngroups').then(process));
|
||||
}
|
||||
else if(["primary_group", "groupmembers"].includes(type))
|
||||
else if(type !== "none")
|
||||
{
|
||||
fetch.push(this.egw().accounts(this.accountType).then(options => {this.static_options = this.static_options.concat(cleanSelectOptions(options))}));
|
||||
fetch.push(this.egw().accounts(this.accountType).then(process));
|
||||
}
|
||||
this.fetchComplete = Promise.all(fetch)
|
||||
.then(() => this._renderOptions());
|
||||
}
|
||||
|
||||
|
||||
firstUpdated(changedProperties?)
|
||||
{
|
||||
super.firstUpdated(changedProperties);
|
||||
// Due to the different way Et2SelectAccount handles options, we call this explicitly
|
||||
this._renderOptions();
|
||||
return Promise.all(fetch).then(() =>
|
||||
{
|
||||
this.requestUpdate("select_options");
|
||||
});
|
||||
}
|
||||
|
||||
set accountType(type : AccountType)
|
||||
@ -102,12 +117,7 @@ export class Et2SelectAccount extends SelectAccountMixin(Et2StaticSelectMixin(Et
|
||||
{
|
||||
return [];
|
||||
}
|
||||
let select_options : Array<SelectOption> = [...(this.static_options || []), ...super.select_options];
|
||||
|
||||
return select_options.filter((value, index, self) =>
|
||||
{
|
||||
return self.findIndex(v => v.value === value.value) === index;
|
||||
});
|
||||
return super.select_options;
|
||||
}
|
||||
|
||||
set select_options(new_options : SelectOption[])
|
17
api/js/etemplate/Et2Select/Select/Et2SelectApp.ts
Normal file
17
api/js/etemplate/Et2Select/Select/Et2SelectApp.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
|
||||
import {cleanSelectOptions} from "../FindSelectOptions";
|
||||
|
||||
export class Et2SelectApp extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
public connectedCallback()
|
||||
{
|
||||
super.connectedCallback()
|
||||
this.fetchComplete = so.app(this, {}).then((options) =>
|
||||
{
|
||||
this.set_static_options(cleanSelectOptions(options));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-app", Et2SelectApp);
|
26
api/js/etemplate/Et2Select/Select/Et2SelectBitwise.ts
Normal file
26
api/js/etemplate/Et2Select/Select/Et2SelectBitwise.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectBitwise extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
/* currently handled server-side */
|
||||
/*
|
||||
set value(new_value)
|
||||
{
|
||||
let oldValue = this._value;
|
||||
let expanded_value = [];
|
||||
let options = this.select_options;
|
||||
for(let index in options)
|
||||
{
|
||||
let right = parseInt(options[index].value);
|
||||
if(!!(new_value & right))
|
||||
{
|
||||
expanded_value.push(right);
|
||||
}
|
||||
}
|
||||
super.value = expanded_value;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
customElements.define("et2-select-bitwise", Et2SelectBitwise);
|
28
api/js/etemplate/Et2Select/Select/Et2SelectBool.ts
Normal file
28
api/js/etemplate/Et2Select/Select/Et2SelectBool.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectBool extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this._static_options = StaticOptions.bool(this);
|
||||
}
|
||||
|
||||
get value()
|
||||
{
|
||||
return super.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean option values are "0" and "1", so change boolean to those
|
||||
* @param {string | string[]} new_value
|
||||
*/
|
||||
set value(new_value)
|
||||
{
|
||||
super.value = new_value ? "1" : "0";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-bool", Et2SelectBool);
|
145
api/js/etemplate/Et2Select/Select/Et2SelectCategory.ts
Normal file
145
api/js/etemplate/Et2Select/Select/Et2SelectCategory.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* EGroupware eTemplate2 - Select Category WebComponent
|
||||
*
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package api
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, nothing, PropertyValues, TemplateResult, unsafeCSS} from "lit";
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
|
||||
import {cleanSelectOptions} from "../FindSelectOptions";
|
||||
import {StaticValue} from "lit/development/static-html";
|
||||
import {literal} from "lit/static-html.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
|
||||
/**
|
||||
* Customised Select widget for categories
|
||||
* This widget gives us category colors and icons in the options and selected value.
|
||||
*/
|
||||
export class Et2SelectCategory extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
...super.styles,
|
||||
css`
|
||||
/* Category color on options */
|
||||
|
||||
sl-option {
|
||||
border-left: 6px solid var(--category-color, transparent);
|
||||
}
|
||||
/* Border on the (single) selected value */
|
||||
|
||||
:host(:not([multiple]))::part(combobox) {
|
||||
border-left: 6px solid var(--category-color, var(--sl-input-border-color));
|
||||
}
|
||||
`
|
||||
]
|
||||
}
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
/**
|
||||
* Include global categories
|
||||
*/
|
||||
globalCategories: {type: Boolean},
|
||||
/**
|
||||
* Show categories from this application. If not set, will be the current application
|
||||
*/
|
||||
application: {type: String},
|
||||
/**
|
||||
* Show categories below this parent category
|
||||
*/
|
||||
parentCat: {type: Number}
|
||||
}
|
||||
}
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
// we should not translate categories name
|
||||
this.noLang = true;
|
||||
}
|
||||
|
||||
async connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
if(typeof this.application == 'undefined')
|
||||
{
|
||||
this.application =
|
||||
// When the widget is first created, it doesn't have a parent and can't find it's instanceManager
|
||||
(this.getInstanceManager() && this.getInstanceManager().app) ||
|
||||
this.egw().app_name();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
willUpdate(changedProperties : PropertyValues)
|
||||
{
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if(changedProperties.has("global_categories") || changedProperties.has("application") || changedProperties.has("parentCat"))
|
||||
{
|
||||
this.fetchComplete = so.cat(this).then(options =>
|
||||
{
|
||||
this._static_options = cleanSelectOptions(options);
|
||||
this.requestUpdate("select_options");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected handleValueChange(e)
|
||||
{
|
||||
super.handleValueChange(e);
|
||||
|
||||
// Just re-draw to get the colors & icon
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom, dynamic styling
|
||||
*
|
||||
* CSS variables are not making it through to options, re-declaring them here works
|
||||
*
|
||||
* @returns {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
protected _styleTemplate() : TemplateResult
|
||||
{
|
||||
return html`
|
||||
<style>
|
||||
${repeat(this.select_options, (option) =>
|
||||
{
|
||||
if(typeof option.color == "undefined" || !option.color)
|
||||
{
|
||||
return nothing;
|
||||
}
|
||||
return unsafeCSS(
|
||||
(this.getValueAsArray().includes(option.value) ? "::part(combobox) { --category-color: " + option.color + ";}" : "") +
|
||||
".cat_" + option.value + " {--category-color: " + option.color + ";}"
|
||||
);
|
||||
})}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a custom tag for when multiple=true
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
public get tagTag() : StaticValue
|
||||
{
|
||||
return literal`et2-category-tag`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-cat", Et2SelectCategory);
|
97
api/js/etemplate/Et2Select/Select/Et2SelectCountry.ts
Normal file
97
api/js/etemplate/Et2Select/Select/Et2SelectCountry.ts
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* EGroupware eTemplate2 - Select Country WebComponent
|
||||
*
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package api
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
|
||||
import {egw} from "../../../jsapi/egw_global";
|
||||
import {SelectOption} from "../FindSelectOptions";
|
||||
import {html} from "lit";
|
||||
|
||||
/**
|
||||
* Customised Select widget for countries
|
||||
* This widget uses CSS from api/templates/default/css/flags.css to set flags
|
||||
*/
|
||||
egw(window).includeCSS("api/templates/default/css/flags.css")
|
||||
|
||||
export class Et2SelectCountry extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
/* Reflect the value so we can use CSS selectors */
|
||||
value: {type: String, reflect: true}
|
||||
}
|
||||
}
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.search = true;
|
||||
|
||||
(<Promise<SelectOption[]>>so.country(this, {}, true)).then(options =>
|
||||
{
|
||||
this._static_options = options
|
||||
this.requestUpdate("select_options");
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the element for the flag
|
||||
*
|
||||
* @param option
|
||||
* @protected
|
||||
*/
|
||||
protected _iconTemplate(option)
|
||||
{
|
||||
return html`
|
||||
<span slot="prefix" part="flag country_${option.value}_flag"
|
||||
style="width: var(--icon-width)">
|
||||
</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to render each option into the select
|
||||
* Override to get flags in
|
||||
*
|
||||
* @param {SelectOption} option
|
||||
* @returns {TemplateResult}
|
||||
*
|
||||
protected _optionTemplate(option : SelectOption) : TemplateResult
|
||||
{
|
||||
// Exclude non-matches when searching
|
||||
if(typeof option.isMatch == "boolean" && !option.isMatch)
|
||||
{
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sl-option
|
||||
part="option"
|
||||
value="${value}"
|
||||
title="${!option.title || this.noLang ? option.title : this.egw().lang(option.title)}"
|
||||
class="${option.class}" .option=${option}
|
||||
.selected=${this.getValueAsArray().some(v => v == value)}
|
||||
?disabled=${option.disabled}
|
||||
>
|
||||
${this._iconTemplate(option)}
|
||||
${this.noLang ? option.label : this.egw().lang(option.label)}
|
||||
</sl-option>`;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
customElements.define("et2-select-country", Et2SelectCountry);
|
14
api/js/etemplate/Et2Select/Select/Et2SelectDay.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectDay.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectDay extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this._static_options = so.day(this, {});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-day", Et2SelectDay);
|
53
api/js/etemplate/Et2Select/Select/Et2SelectDayOfWeek.ts
Normal file
53
api/js/etemplate/Et2Select/Select/Et2SelectDayOfWeek.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
import {cleanSelectOptions} from "../FindSelectOptions";
|
||||
|
||||
export class Et2SelectDayOfWeek extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
// Wait for connected instead of constructor because attributes make a difference in
|
||||
// which options are offered
|
||||
this.fetchComplete = StaticOptions.dow(this, {other: this.other || []}).then(options =>
|
||||
{
|
||||
this.set_static_options(cleanSelectOptions(options));
|
||||
});
|
||||
}
|
||||
|
||||
set value(new_value)
|
||||
{
|
||||
let expanded_value = typeof new_value == "object" ? new_value : [];
|
||||
if(new_value && (typeof new_value == "string" || typeof new_value == "number"))
|
||||
{
|
||||
let int_value = parseInt(new_value);
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
this.fetchComplete.then(() =>
|
||||
{
|
||||
let options = this.select_options;
|
||||
for(let index in options)
|
||||
{
|
||||
let right = parseInt(options[index].value);
|
||||
|
||||
if((int_value & right) == right)
|
||||
{
|
||||
expanded_value.push("" + right);
|
||||
}
|
||||
}
|
||||
super.value = expanded_value;
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
super.value = expanded_value;
|
||||
}
|
||||
|
||||
get value()
|
||||
{
|
||||
return super.value;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-dow", Et2SelectDayOfWeek);
|
@ -7,11 +7,12 @@
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
import {Et2Select} from "./Et2Select";
|
||||
import {css, html, nothing, PropertyValues} from "@lion/core";
|
||||
import {IsEmail} from "../Validators/IsEmail";
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {css, html, nothing, PropertyValues} from "lit";
|
||||
import {IsEmail} from "../../Validators/IsEmail";
|
||||
import interact from "@interactjs/interact";
|
||||
import {Validator} from "@lion/form-core";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
|
||||
/**
|
||||
* Select email address(es)
|
||||
@ -100,6 +101,7 @@ export class Et2SelectEmail extends Et2Select
|
||||
this.defaultValidators.push(new IsEmail(this.allowPlaceholder));
|
||||
}
|
||||
|
||||
|
||||
/** @param {import('@lion/core').PropertyValues } changedProperties */
|
||||
willUpdate(changedProperties : PropertyValues)
|
||||
{
|
||||
@ -112,9 +114,38 @@ export class Et2SelectEmail extends Et2Select
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
updated(changedProperties : Map<string, any>)
|
||||
{
|
||||
super.connectedCallback();
|
||||
// Make tags draggable
|
||||
if(!this.readonly && this.allowFreeEntries && this.allowDragAndDrop)
|
||||
{
|
||||
let dragTranslate = {x: 0, y: 0};
|
||||
const tags = this.shadowRoot.querySelectorAll(".select__tags [part='tag']");
|
||||
let draggable = interact(tags).draggable({
|
||||
startAxis: 'xy',
|
||||
listeners: {
|
||||
start: function(e)
|
||||
{
|
||||
let dragPosition = {x: e.page.x, y: e.page.y};
|
||||
dragTranslate = {x: 0, y: 0};
|
||||
e.target.setAttribute('style', `width:${e.target.clientWidth}px !important`);
|
||||
e.target.style.position = 'fixed';
|
||||
e.target.style.zIndex = 10;
|
||||
e.target.style.transform =
|
||||
`translate(${dragPosition.x}px, ${dragPosition.y}px)`;
|
||||
},
|
||||
move: function(e)
|
||||
{
|
||||
dragTranslate.x += e.delta.x;
|
||||
dragTranslate.y += e.delta.y;
|
||||
e.target.style.transform =
|
||||
`translate(${dragTranslate.x}px, ${dragTranslate.y}px)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
// set parent_node with widget context in order to make it accessible after drop
|
||||
draggable.parent_node = this;
|
||||
}
|
||||
}
|
||||
|
||||
protected _bindListeners()
|
||||
@ -145,7 +176,7 @@ export class Et2SelectEmail extends Et2Select
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle keypresses inside the search input
|
||||
* Overridden from parent to also skip the hidden selected options, which other selects do not do
|
||||
@ -187,55 +218,27 @@ export class Et2SelectEmail extends Et2Select
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get tagTag() : string
|
||||
_tagTemplate(option, index)
|
||||
{
|
||||
return "et2-email-tag";
|
||||
}
|
||||
const readonly = (this.readonly || option && typeof (option.disabled) != "undefined" && option.disabled);
|
||||
const isEditable = this.editModeEnabled && !readonly;
|
||||
|
||||
/**
|
||||
* override tag creation in order to add DND functionality
|
||||
* @param item
|
||||
* @protected
|
||||
*/
|
||||
protected _createTagNode(item)
|
||||
{
|
||||
let tag = super._createTagNode(item);
|
||||
|
||||
tag.fullEmail = this.fullEmail;
|
||||
tag.onlyEmail = this.onlyEmail;
|
||||
|
||||
// Re-set after setting fullEmail as that can change what we show
|
||||
tag.textContent = item.getTextLabel().trim();
|
||||
|
||||
if(!this.readonly && this.allowFreeEntries && this.allowDragAndDrop)
|
||||
{
|
||||
let dragTranslate = {x: 0, y: 0};
|
||||
tag.class = item.classList.value + " et2-select-draggable";
|
||||
let draggable = interact(tag).draggable({
|
||||
startAxis: 'xy',
|
||||
listeners: {
|
||||
start: function(e)
|
||||
{
|
||||
let dragPosition = {x:e.page.x, y:e.page.y};
|
||||
e.target.setAttribute('style', `width:${e.target.clientWidth}px !important`);
|
||||
e.target.style.position = 'fixed';
|
||||
e.target.style.zIndex = 10;
|
||||
e.target.style.transform =
|
||||
`translate(${dragPosition.x}px, ${dragPosition.y}px)`;
|
||||
},
|
||||
move : function(e)
|
||||
{
|
||||
dragTranslate.x += e.delta.x;
|
||||
dragTranslate.y += e.delta.y;
|
||||
e.target.style.transform =
|
||||
`translate(${dragTranslate.x}px, ${dragTranslate.y}px)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
// set parent_node with widget context in order to make it accessible after drop
|
||||
draggable.parent_node = this;
|
||||
}
|
||||
return tag;
|
||||
return html`
|
||||
<et2-email-tag
|
||||
class=${classMap({
|
||||
...option.classList,
|
||||
"et2-select-draggable": !this.readonly && this.allowFreeEntries && this.allowDragAndDrop
|
||||
})}
|
||||
.fullEmail=${this.fullEmail}
|
||||
.onlyEmail=${this.onlyEmail}
|
||||
?removable=${!readonly}
|
||||
?readonly=${readonly}
|
||||
?editable=${isEditable}
|
||||
.value=${option.value.replaceAll("___", " ")}
|
||||
>
|
||||
${option.getTextLabel().trim()}
|
||||
</et2-email-tag>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,16 +261,6 @@ export class Et2SelectEmail extends Et2Select
|
||||
</et2-lavatar>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override image to skip it, we add images in Et2EmailTag using CSS
|
||||
* @param item
|
||||
* @protected
|
||||
*/
|
||||
protected _createImage(item)
|
||||
{
|
||||
return this.multiple ? "" : super._createImage(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwritten to NOT split RFC822 addresses containing a comma in quoted name part
|
||||
*
|
14
api/js/etemplate/Et2Select/Select/Et2SelectHour.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectHour.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectHour extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this._static_options = StaticOptions.hour(this, {other: this.other || []});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-hour", Et2SelectHour);
|
14
api/js/etemplate/Et2Select/Select/Et2SelectLang.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectLang.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectLang extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this._static_options = StaticOptions.lang(this, {other: this.other || []});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-lang", Et2SelectLang);
|
14
api/js/etemplate/Et2Select/Select/Et2SelectMonth.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectMonth.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectMonth extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this._static_options = StaticOptions.month(this);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-month", Et2SelectMonth);
|
52
api/js/etemplate/Et2Select/Select/Et2SelectNumber.ts
Normal file
52
api/js/etemplate/Et2Select/Select/Et2SelectNumber.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
import {PropertyValues} from 'lit';
|
||||
|
||||
export class Et2SelectNumber extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
/**
|
||||
* Step between numbers
|
||||
*/
|
||||
interval: {type: Number},
|
||||
min: {type: Number},
|
||||
max: {type: Number},
|
||||
|
||||
/**
|
||||
* Add one or more leading zeros
|
||||
* Set to how many zeros you want (000)
|
||||
*/
|
||||
leading_zero: {type: String},
|
||||
/**
|
||||
* Appended after every number
|
||||
*/
|
||||
suffix: {type: String}
|
||||
}
|
||||
}
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.min = 1;
|
||||
this.max = 10;
|
||||
this.interval = 1;
|
||||
this.leading_zero = "";
|
||||
this.suffix = "";
|
||||
}
|
||||
|
||||
updated(changedProperties : PropertyValues)
|
||||
{
|
||||
super.updated(changedProperties);
|
||||
|
||||
if(changedProperties.has('min') || changedProperties.has('max') || changedProperties.has('interval') || changedProperties.has('suffix'))
|
||||
{
|
||||
this._static_options = StaticOptions.number(this);
|
||||
this.requestUpdate("select_options");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-number", Et2SelectNumber);
|
15
api/js/etemplate/Et2Select/Select/Et2SelectPercent.ts
Normal file
15
api/js/etemplate/Et2Select/Select/Et2SelectPercent.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {Et2SelectNumber} from "./Et2SelectNumber";
|
||||
|
||||
export class Et2SelectPercent extends Et2SelectNumber
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.min = 0;
|
||||
this.max = 100;
|
||||
this.interval = 10;
|
||||
this.suffix = "%%";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-percent", Et2SelectPercent);
|
14
api/js/etemplate/Et2Select/Select/Et2SelectPriority.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectPriority.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectPriority extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this._static_options = StaticOptions.priority(this);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-priority", Et2SelectPriority);
|
@ -8,12 +8,13 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, LitElement, repeat, TemplateResult} from "@lion/core";
|
||||
import {et2_IDetachedDOM} from "../et2_core_interfaces";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {Et2StaticSelectMixin, StaticOptions, StaticOptions as so} from "./StaticOptions";
|
||||
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
|
||||
import {SelectAccountMixin} from "./SelectAccountMixin";
|
||||
import {css, html, LitElement, TemplateResult} from "lit";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {et2_IDetachedDOM} from "../../et2_core_interfaces";
|
||||
import {Et2Widget} from "../../Et2Widget/Et2Widget";
|
||||
import {Et2StaticSelectMixin, StaticOptions, StaticOptions as so} from "../StaticOptions";
|
||||
import {cleanSelectOptions, find_select_options, SelectOption} from "../FindSelectOptions";
|
||||
import {SelectAccountMixin} from "../SelectAccountMixin";
|
||||
|
||||
/**
|
||||
* This is a stripped-down read-only widget used in nextmatch
|
||||
@ -143,6 +144,11 @@ li {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getValueAsArray()
|
||||
{
|
||||
return (Array.isArray(this.value) ? this.value : [this.value]);
|
||||
}
|
||||
|
||||
set value(new_value : string | string[])
|
||||
{
|
||||
// Split anything that is still a CSV
|
||||
@ -206,10 +212,11 @@ li {
|
||||
|
||||
render()
|
||||
{
|
||||
const value = this.getValueAsArray();
|
||||
return html`
|
||||
<ul>
|
||||
${repeat(
|
||||
(Array.isArray(this.value) ? this.value : [this.value]),
|
||||
this.getValueAsArray(),
|
||||
(val : string) => val, (val) =>
|
||||
{
|
||||
let option = (<SelectOption[]>this.select_options).find(option => option.value == val);
|
||||
@ -282,14 +289,16 @@ customElements.define("et2-select-app_ro", Et2SelectAppReadonly);
|
||||
|
||||
export class Et2SelectBitwiseReadonly extends Et2SelectReadonly
|
||||
{
|
||||
/* Currently handled server side, we get an array
|
||||
render()
|
||||
{
|
||||
let new_value = [];
|
||||
let int_value = parseInt(this.value);
|
||||
for(let index in this.select_options)
|
||||
{
|
||||
let option = this.select_options[index];
|
||||
let right = parseInt(option && option.value ? option.value : index);
|
||||
if(!!(this.value & right))
|
||||
if(!!(int_value & right))
|
||||
{
|
||||
new_value.push(right);
|
||||
}
|
||||
@ -307,6 +316,8 @@ export class Et2SelectBitwiseReadonly extends Et2SelectReadonly
|
||||
})}
|
||||
</ul>`;
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||
@ -349,7 +360,6 @@ export class Et2SelectPercentReadonly extends Et2SelectReadonly
|
||||
constructor()
|
||||
{
|
||||
super(...arguments);
|
||||
this.suffix = "%%";
|
||||
this.select_options = so.percent(this);
|
||||
}
|
||||
}
|
||||
@ -391,6 +401,23 @@ export class Et2SelectDayOfWeekReadonly extends Et2StaticSelectMixin(Et2SelectRe
|
||||
this.set_static_options(cleanSelectOptions(options));
|
||||
});
|
||||
}
|
||||
|
||||
getValueAsArray()
|
||||
{
|
||||
let expanded_value = [];
|
||||
let int_value = parseInt(this.value);
|
||||
let options = this.select_options;
|
||||
for(let index in options)
|
||||
{
|
||||
let right = parseInt(<string>options[index].value);
|
||||
|
||||
if((int_value & right) == right)
|
||||
{
|
||||
expanded_value.push("" + right);
|
||||
}
|
||||
}
|
||||
return expanded_value;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||
@ -422,7 +449,7 @@ export class Et2SelectNumberReadonly extends Et2StaticSelectMixin(Et2SelectReado
|
||||
{
|
||||
protected find_select_options(_attrs)
|
||||
{
|
||||
this.static_options = so.number(this, _attrs);
|
||||
this._static_options = so.number(this, _attrs);
|
||||
}
|
||||
}
|
||||
|
45
api/js/etemplate/Et2Select/Select/Et2SelectState.ts
Normal file
45
api/js/etemplate/Et2Select/Select/Et2SelectState.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
import {SelectOption} from "../FindSelectOptions";
|
||||
|
||||
export class Et2SelectState extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
/**
|
||||
* Two-letter ISO country code
|
||||
*/
|
||||
protected __countryCode;
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
countryCode: String,
|
||||
}
|
||||
}
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.countryCode = 'DE';
|
||||
}
|
||||
|
||||
get countryCode()
|
||||
{
|
||||
return this.__countryCode;
|
||||
}
|
||||
|
||||
set countryCode(code : string)
|
||||
{
|
||||
this.__countryCode = code;
|
||||
this._static_options = <SelectOption[]>StaticOptions.state(this, {country_code: code});
|
||||
this.requestUpdate("select_options");
|
||||
}
|
||||
|
||||
set_country_code(code)
|
||||
{
|
||||
this.countryCode = code;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-state", Et2SelectState);
|
61
api/js/etemplate/Et2Select/Select/Et2SelectTab.ts
Normal file
61
api/js/etemplate/Et2Select/Select/Et2SelectTab.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {Et2SelectApp} from "./Et2SelectApp";
|
||||
import {SelectOption} from "../FindSelectOptions";
|
||||
|
||||
export class Et2SelectTab extends Et2SelectApp
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.allowFreeEntries = true;
|
||||
}
|
||||
|
||||
set value(new_value)
|
||||
{
|
||||
if(!new_value)
|
||||
{
|
||||
super.value = new_value;
|
||||
return;
|
||||
}
|
||||
const values = Array.isArray(new_value) ? new_value : [new_value];
|
||||
const options = this.select_options;
|
||||
values.forEach(value =>
|
||||
{
|
||||
if(!options.filter(option => option.value == value).length)
|
||||
{
|
||||
const matches = value.match(/^([a-z0-9]+)\-/i);
|
||||
let option : SelectOption = {value: value, label: value};
|
||||
if(matches)
|
||||
{
|
||||
option = options.filter(option => option.value == matches[1])[0] || {
|
||||
value: value,
|
||||
label: this.egw().lang(matches[1])
|
||||
};
|
||||
option.value = value;
|
||||
option.label += ' ' + this.egw().lang('Tab');
|
||||
}
|
||||
try
|
||||
{
|
||||
const app = opener?.framework.getApplicationByName(value);
|
||||
if(app && app.displayName)
|
||||
{
|
||||
option.label = app.displayName;
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// ignore security exception, if opener is not accessible
|
||||
}
|
||||
this.select_options.concat(option);
|
||||
}
|
||||
})
|
||||
super.value = new_value;
|
||||
}
|
||||
|
||||
get value()
|
||||
{
|
||||
return super.value;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-tab", Et2SelectTab);
|
@ -7,9 +7,9 @@
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
import {Et2Select} from "./Et2Select";
|
||||
import {css} from "@lion/core";
|
||||
import {SelectOption} from "./FindSelectOptions";
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {css} from "lit";
|
||||
import {SelectOption} from "../FindSelectOptions";
|
||||
|
||||
export class Et2SelectThumbnail extends Et2Select
|
||||
{
|
17
api/js/etemplate/Et2Select/Select/Et2SelectTimezone.ts
Normal file
17
api/js/etemplate/Et2Select/Select/Et2SelectTimezone.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
|
||||
import {cleanSelectOptions} from "../FindSelectOptions";
|
||||
|
||||
export class Et2SelectTimezone extends Et2StaticSelectMixin(Et2Select)
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.fetchComplete = StaticOptions.timezone(this, {other: this.other ?? []}).then((options) =>
|
||||
{
|
||||
this.set_static_options(cleanSelectOptions(options));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-timezone", Et2SelectTimezone);
|
25
api/js/etemplate/Et2Select/Select/Et2SelectYear.ts
Normal file
25
api/js/etemplate/Et2Select/Select/Et2SelectYear.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {Et2SelectNumber} from "./Et2SelectNumber";
|
||||
import {PropertyValues} from "lit";
|
||||
import {StaticOptions} from "../StaticOptions";
|
||||
|
||||
export class Et2SelectYear extends Et2SelectNumber
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.min = -3;
|
||||
this.max = 2;
|
||||
}
|
||||
|
||||
updated(changedProperties : PropertyValues)
|
||||
{
|
||||
super.updated(changedProperties);
|
||||
|
||||
if(changedProperties.has('min') || changedProperties.has('max') || changedProperties.has('interval') || changedProperties.has('suffix'))
|
||||
{
|
||||
this._static_options = StaticOptions.year(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-select-year", Et2SelectYear);
|
@ -1,5 +1,5 @@
|
||||
import {SelectOption} from "./FindSelectOptions";
|
||||
import {LitElement} from "@lion/core";
|
||||
import {LitElement} from "lit";
|
||||
|
||||
/**
|
||||
* EGroupware eTemplate2 - SelectAccountMixin
|
||||
@ -110,7 +110,8 @@ export const SelectAccountMixin = <T extends Constructor<LitElement>>(superclass
|
||||
|
||||
get select_options()
|
||||
{
|
||||
return [...(this.account_options || []), ...super.select_options];
|
||||
return [...new Map([...this.account_options, ...(super.select_options || [])].map(item =>
|
||||
[item.value, item])).values()];
|
||||
}
|
||||
|
||||
set select_options(value : SelectOption[])
|
||||
|
25
api/js/etemplate/Et2Select/SelectTypes.ts
Normal file
25
api/js/etemplate/Et2Select/SelectTypes.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Import all our sub-types
|
||||
*/
|
||||
|
||||
import './Select/Et2SelectAccount';
|
||||
import './Select/Et2SelectApp';
|
||||
import './Select/Et2SelectBitwise';
|
||||
import './Select/Et2SelectBool';
|
||||
import './Select/Et2SelectCategory';
|
||||
import './Select/Et2SelectCountry';
|
||||
import './Select/Et2SelectDay';
|
||||
import './Select/Et2SelectDayOfWeek';
|
||||
import './Select/Et2SelectEmail';
|
||||
import './Select/Et2SelectHour';
|
||||
import './Select/Et2SelectLang';
|
||||
import './Select/Et2SelectMonth';
|
||||
import './Select/Et2SelectNumber';
|
||||
import './Select/Et2SelectPercent';
|
||||
import './Select/Et2SelectPriority';
|
||||
import './Select/Et2SelectReadonly';
|
||||
import './Select/Et2SelectState';
|
||||
import './Select/Et2SelectTab';
|
||||
import './Select/Et2SelectThumbnail';
|
||||
import './Select/Et2SelectTimezone';
|
||||
import './Select/Et2SelectYear';
|
@ -8,11 +8,18 @@
|
||||
* @param {type} widget
|
||||
*/
|
||||
import {sprintf} from "../../egw_action/egw_action_common";
|
||||
import {Et2SelectReadonly} from "./Et2SelectReadonly";
|
||||
import {Et2SelectReadonly} from "./Select/Et2SelectReadonly";
|
||||
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
|
||||
import {Et2Select, Et2WidgetWithSelect} from "./Et2Select";
|
||||
import {state} from "lit/decorators/state.js";
|
||||
|
||||
export type Et2SelectWidgets = Et2Select | Et2WidgetWithSelect | Et2SelectReadonly;
|
||||
type NumberOptions = {
|
||||
min? : number,
|
||||
max? : number,
|
||||
interval? : number,
|
||||
format? : string
|
||||
};
|
||||
|
||||
// Export the Interface for TypeScript
|
||||
type Constructor<T = {}> = new (...args : any[]) => T;
|
||||
@ -31,27 +38,25 @@ export const Et2StaticSelectMixin = <T extends Constructor<Et2WidgetWithSelect>>
|
||||
|
||||
// Hold the static widget options separately so other options (like sent from server in sel_options) won't
|
||||
// conflict or be wiped out
|
||||
protected static_options : SelectOption[];
|
||||
@state()
|
||||
protected _static_options : SelectOption[] = [];
|
||||
|
||||
// If widget needs to fetch options from server, we might want to wait for them
|
||||
protected fetchComplete : Promise<SelectOption[] | void>;
|
||||
@state()
|
||||
protected fetchComplete : Promise<SelectOption[] | void> = Promise.resolve();
|
||||
|
||||
constructor(...args)
|
||||
async getUpdateComplete() : Promise<boolean>
|
||||
{
|
||||
super(...args);
|
||||
|
||||
this.static_options = [];
|
||||
this.fetchComplete = Promise.resolve();
|
||||
|
||||
// Trigger the options to get rendered into the DOM
|
||||
this.requestUpdate("select_options");
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.fetchComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
get select_options() : SelectOption[]
|
||||
{
|
||||
// @ts-ignore
|
||||
const options = super.select_options || [];
|
||||
const statics = this.static_options || [];
|
||||
const statics = this._static_options || [];
|
||||
|
||||
if(options.length == 0)
|
||||
{
|
||||
@ -62,7 +67,7 @@ export const Et2StaticSelectMixin = <T extends Constructor<Et2WidgetWithSelect>>
|
||||
return options;
|
||||
}
|
||||
// Merge & make sure result is unique
|
||||
return [...new Map([...options, ...statics].map(item =>
|
||||
return [...new Map([...options, ...(this._static_options || [])].map(item =>
|
||||
[item.value, item])).values()];
|
||||
|
||||
}
|
||||
@ -75,7 +80,7 @@ export const Et2StaticSelectMixin = <T extends Constructor<Et2WidgetWithSelect>>
|
||||
|
||||
set_static_options(new_static_options)
|
||||
{
|
||||
this.static_options = new_static_options;
|
||||
this._static_options = new_static_options;
|
||||
this.requestUpdate("select_options");
|
||||
}
|
||||
|
||||
@ -273,19 +278,14 @@ export const StaticOptions = new class StaticOptionsType
|
||||
];
|
||||
}
|
||||
|
||||
number(widget : Et2SelectWidgets, attrs = {
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
interval: undefined,
|
||||
format: undefined
|
||||
}) : SelectOption[]
|
||||
number(widget : Et2SelectWidgets, attrs : NumberOptions = {}) : SelectOption[]
|
||||
{
|
||||
|
||||
var options = [];
|
||||
var min = parseFloat(attrs.min ?? widget.min ?? 1);
|
||||
var max = parseFloat(attrs.max ?? widget.max ?? 10);
|
||||
var interval = parseFloat(attrs.interval ?? widget.interval ?? 1);
|
||||
var format = attrs.format ?? '%d';
|
||||
const options = [];
|
||||
const min = parseFloat(attrs.min ?? widget.min ?? 1);
|
||||
const max = parseFloat(attrs.max ?? widget.max ?? 10);
|
||||
let interval = parseFloat(attrs.interval ?? widget.interval ?? 1);
|
||||
let format = attrs.format ?? '%d';
|
||||
|
||||
// leading zero specified in interval
|
||||
if(widget.leading_zero && widget.leading_zero[0] == '0')
|
||||
@ -313,7 +313,7 @@ export const StaticOptions = new class StaticOptionsType
|
||||
|
||||
percent(widget : Et2SelectWidgets) : SelectOption[]
|
||||
{
|
||||
return this.number(widget, {min: 0, max: 100, interval: 10, format: undefined});
|
||||
return this.number(widget, {min: 0, max: 100, interval: 10, format: "%d%%"});
|
||||
}
|
||||
|
||||
year(widget : Et2SelectWidgets, attrs?) : SelectOption[]
|
||||
@ -323,15 +323,14 @@ export const StaticOptions = new class StaticOptionsType
|
||||
attrs = {}
|
||||
}
|
||||
var t = new Date();
|
||||
attrs.min = t.getFullYear() + parseInt(widget.min);
|
||||
attrs.max = t.getFullYear() + parseInt(widget.max);
|
||||
attrs.min = t.getFullYear() + parseInt(attrs.min ?? widget.min ?? -3);
|
||||
attrs.max = t.getFullYear() + parseInt(attrs.max ?? widget.max ?? 2);
|
||||
return this.number(widget, attrs);
|
||||
}
|
||||
|
||||
day(widget : Et2SelectWidgets, attrs) : SelectOption[]
|
||||
{
|
||||
attrs.other = [1, 31, 1];
|
||||
return this.number(widget, attrs);
|
||||
return this.number(widget, {min: 1, max: 31, interval: 1});
|
||||
}
|
||||
|
||||
hour(widget : Et2SelectWidgets, attrs) : SelectOption[]
|
||||
@ -394,9 +393,9 @@ export const StaticOptions = new class StaticOptionsType
|
||||
return this.cached_server_side(widget, 'select-lang', options);
|
||||
}
|
||||
|
||||
timezone(widget : Et2SelectWidgets, attrs) : SelectOption[] | Promise<SelectOption[]>
|
||||
timezone(widget : Et2SelectWidgets, attrs) : Promise<SelectOption[]>
|
||||
{
|
||||
var options = ',' + (attrs.other || []).join(',');
|
||||
return this.cached_server_side(widget, 'select-timezone', options);
|
||||
return <Promise<SelectOption[]>>this.cached_server_side(widget, 'select-timezone', options, true);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
import {css, html, TemplateResult} from "@lion/core";
|
||||
import {css, html, TemplateResult} from "lit";
|
||||
import shoelace from "../../Styles/shoelace";
|
||||
import {Et2Tag} from "./Et2Tag";
|
||||
|
||||
|
@ -6,7 +6,8 @@
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
import {classMap, css, html, nothing, PropertyValues, TemplateResult} from "@lion/core";
|
||||
import {css, html, nothing, PropertyValues, TemplateResult} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import shoelace from "../../Styles/shoelace";
|
||||
import {Et2Tag} from "./Et2Tag";
|
||||
|
||||
@ -56,6 +57,15 @@ export class Et2EmailTag extends Et2Tag
|
||||
.tag__remove {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
/* Shoelace disabled gives a not-allowed cursor, but we also set disabled for read-only.
|
||||
* We don't want the not-allowed cursor, since you can always click the email address
|
||||
*/
|
||||
|
||||
:host([readonly]) {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
`];
|
||||
}
|
||||
|
||||
@ -95,8 +105,8 @@ export class Et2EmailTag extends Et2Tag
|
||||
this.onlyEmail = false;
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleContactClick = this.handleContactClick.bind(this);
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
this.handleContactMouseDown = this.handleContactMouseDown.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
@ -166,7 +176,7 @@ export class Et2EmailTag extends Et2Tag
|
||||
this.shadowRoot.querySelector(".tag").classList.remove("contact_plus");
|
||||
}
|
||||
|
||||
handleClick(e : MouseEvent)
|
||||
handleMouseDown(e : MouseEvent)
|
||||
{
|
||||
e.stopPropagation();
|
||||
|
||||
@ -177,7 +187,7 @@ export class Et2EmailTag extends Et2Tag
|
||||
this.egw().open('', 'addressbook', 'add', extra);
|
||||
}
|
||||
|
||||
handleContactClick(e : MouseEvent)
|
||||
handleContactMouseDown(e : MouseEvent)
|
||||
{
|
||||
e.stopPropagation();
|
||||
this.checkContact(this.value).then((result) =>
|
||||
@ -217,15 +227,15 @@ export class Et2EmailTag extends Et2Tag
|
||||
{
|
||||
let content = this.value;
|
||||
// If there's a name, just show the name, otherwise show the email
|
||||
if(!this.onlyEmail && Et2EmailTag.email_cache[this.value])
|
||||
if(!this.onlyEmail && Et2EmailTag.email_cache[content])
|
||||
{
|
||||
// Append current value as email, data may have work & home email in it
|
||||
content = (Et2EmailTag.email_cache[this.value]?.n_fn || "") + " <" + (Et2EmailTag.splitEmail(this.value)?.email || this.value) + ">"
|
||||
content = (Et2EmailTag.email_cache[content]?.n_fn || "") + " <" + (Et2EmailTag.splitEmail(content)?.email || content) + ">"
|
||||
}
|
||||
if (this.onlyEmail)
|
||||
{
|
||||
const split = Et2EmailTag.splitEmail(content);
|
||||
content = split.email || this.value;
|
||||
content = split.email || content;
|
||||
}
|
||||
else if(!this.fullEmail)
|
||||
{
|
||||
@ -255,7 +265,7 @@ export class Et2EmailTag extends Et2Tag
|
||||
|
||||
button_or_avatar = html`
|
||||
<et2-lavatar slot="prefix" part="icon"
|
||||
@click=${this.handleContactClick}
|
||||
@mousedown=${this.handleContactMouseDown}
|
||||
.size=${style.getPropertyValue("--icon-width")}
|
||||
lname=${option.lname || nothing}
|
||||
fname=${option.fname || nothing}
|
||||
@ -269,7 +279,7 @@ export class Et2EmailTag extends Et2Tag
|
||||
// Show a button to add as new contact
|
||||
classes['tag__has_plus'] = true;
|
||||
button_or_avatar = html`
|
||||
<et2-button-icon image="add" @click=${this.handleClick}
|
||||
<et2-button-icon image="add" @mousedown=${this.handleMouseDown}
|
||||
label="${this.egw().lang("Add a new contact")}"
|
||||
statustext="${this.egw().lang("Add a new contact")}">
|
||||
</et2-button-icon>`;
|
||||
|
@ -8,7 +8,8 @@
|
||||
*/
|
||||
import {Et2Widget} from "../../Et2Widget/Et2Widget";
|
||||
import {SlTag} from "@shoelace-style/shoelace";
|
||||
import {classMap, css, html, TemplateResult} from "@lion/core";
|
||||
import {css, html, TemplateResult} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import shoelace from "../../Styles/shoelace";
|
||||
|
||||
/**
|
||||
@ -23,7 +24,6 @@ export class Et2Tag extends Et2Widget(SlTag)
|
||||
shoelace, css`
|
||||
:host {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tag--pill {
|
||||
@ -35,6 +35,9 @@ export class Et2Tag extends Et2Widget(SlTag)
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.tag__prefix {
|
||||
line-height: normal;
|
||||
}
|
||||
.tag__content {
|
||||
padding: 0px 0.2rem;
|
||||
flex: 1 2 auto;
|
||||
@ -113,11 +116,12 @@ export class Et2Tag extends Et2Widget(SlTag)
|
||||
<sl-icon-button
|
||||
part="remove-button"
|
||||
exportparts="base:remove-button__base"
|
||||
name="x"
|
||||
name="x-lg"
|
||||
library="system"
|
||||
label=${this.egw().lang('remove')}
|
||||
class="tag__remove"
|
||||
@click=${this.handleRemoveClick}
|
||||
tabindex="-1"
|
||||
></sl-icon-button>
|
||||
`
|
||||
: ''}
|
||||
|
@ -6,7 +6,7 @@
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import shoelace from "../../Styles/shoelace";
|
||||
import {Et2Tag} from "./Et2Tag";
|
||||
|
||||
|
@ -13,6 +13,7 @@ window.egw = {
|
||||
};
|
||||
|
||||
let element : Et2Select;
|
||||
const tag_name = "et2-tag";
|
||||
|
||||
async function before(editable = true)
|
||||
{
|
||||
@ -20,16 +21,19 @@ async function before(editable = true)
|
||||
// @ts-ignore
|
||||
element = await fixture<Et2Select>(html`
|
||||
<et2-select label="I'm a select" value="one" multiple="true" .editModeEnabled=${editable}>
|
||||
<sl-menu-item value="one">One</sl-menu-item>
|
||||
<sl-menu-item value="two">Two</sl-menu-item>
|
||||
<option value="one">One</option>
|
||||
<option value="two">Two</option>
|
||||
</et2-select>
|
||||
`);
|
||||
// Need to call loadFromXML() explicitly to read the options
|
||||
element.loadFromXML(element);
|
||||
|
||||
// Stub egw()
|
||||
sinon.stub(element, "egw").returns(window.egw);
|
||||
|
||||
await element.updateComplete;
|
||||
let tags = [];
|
||||
element.shadowRoot.querySelectorAll(element.tagTag).forEach((t : Et2Tag) => tags.push(t.updateComplete));
|
||||
element.shadowRoot.querySelectorAll(tag_name).forEach((t : Et2Tag) => tags.push(t.updateComplete));
|
||||
await Promise.all(tags);
|
||||
|
||||
return element;
|
||||
@ -48,30 +52,29 @@ describe("Editable tag", () =>
|
||||
|
||||
it("Tag editable matches editModeEnabled", async() =>
|
||||
{
|
||||
let tag = element.shadowRoot.querySelectorAll(element.tagTag);
|
||||
let tag = element.select.combobox.querySelectorAll(tag_name);
|
||||
assert.isAbove(tag.length, 0, "No tags found");
|
||||
assert.isTrue(tag[0].editable);
|
||||
|
||||
// Change it to false & force immediate update
|
||||
element.editModeEnabled = false;
|
||||
element.syncItemsFromValue();
|
||||
element.requestUpdate();
|
||||
await element.updateComplete;
|
||||
|
||||
tag = element.shadowRoot.querySelectorAll(element.tagTag);
|
||||
tag = element.select.combobox.querySelectorAll(tag_name);
|
||||
assert.isAbove(tag.length, 0, "No tags found");
|
||||
assert.isFalse(tag[0].editable);
|
||||
});
|
||||
|
||||
it("Has edit button when editable ", async() =>
|
||||
{
|
||||
let tag = element.shadowRoot.querySelectorAll(element.tagTag);
|
||||
let tag = element.select.combobox.querySelectorAll(tag_name);
|
||||
assert.isAbove(tag.length, 0, "No tags found");
|
||||
assert.exists(tag[0].shadowRoot.querySelector("et2-button-icon[label='edit*']"), "No edit button");
|
||||
});
|
||||
it("Shows input when edit button is clicked", async() =>
|
||||
{
|
||||
let tag = element.shadowRoot.querySelectorAll(element.tagTag)[0];
|
||||
let tag = element.select.combobox.querySelectorAll(tag_name)[0];
|
||||
|
||||
let edit_button = tag.shadowRoot.querySelector("et2-button-icon");
|
||||
edit_button.click();
|
||||
@ -81,7 +84,7 @@ describe("Editable tag", () =>
|
||||
});
|
||||
it("Changes value when edited", async() =>
|
||||
{
|
||||
let tag = <Et2Tag>element.shadowRoot.querySelectorAll(element.tagTag)[0];
|
||||
let tag = <Et2Tag>element.select.combobox.querySelectorAll(tag_name)[0];
|
||||
tag.isEditing = true;
|
||||
tag.requestUpdate();
|
||||
await tag.updateComplete;
|
||||
@ -119,7 +122,7 @@ describe("Editable tag", () =>
|
||||
await listener2;
|
||||
assert.equal(tag.value, "change select too");
|
||||
|
||||
// Haven't turned on allow free entries, so no change here
|
||||
// Have turned on allow free entries, so it should change here
|
||||
assert.equal(element.value, "change select too", "Tag change did not cause value change in parent select (allowFreeEntries was on)");
|
||||
|
||||
});
|
||||
@ -129,7 +132,7 @@ describe("Editable tag", () =>
|
||||
element.readonly = true;
|
||||
await element.updateComplete;
|
||||
|
||||
let tag = element.shadowRoot.querySelectorAll(element.tagTag);
|
||||
let tag = element.select.combobox.querySelectorAll(tag_name);
|
||||
assert.isAbove(tag.length, 0, "No tags found");
|
||||
|
||||
let wait = [];
|
||||
@ -146,7 +149,7 @@ describe("Select is not editable", () =>
|
||||
|
||||
it("Does not have edit button when not editable", async() =>
|
||||
{
|
||||
let tag = element.shadowRoot.querySelectorAll(element.tagTag);
|
||||
let tag = element.select.combobox.querySelectorAll(tag_name);
|
||||
assert.isAbove(tag.length, 0, "No tags found");
|
||||
|
||||
assert.isNull(tag[0].shadowRoot.querySelector("et2-button-icon[label='edit*']"), "Unexpected edit button");
|
||||
|
104
api/js/etemplate/Et2Select/test/Et2EmailTag.test.ts
Normal file
104
api/js/etemplate/Et2Select/test/Et2EmailTag.test.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* EGroupware eTemplate2 - Email Tag WebComponent tests
|
||||
*
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package api
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
import {assert, fixture, html} from '@open-wc/testing';
|
||||
import {Et2EmailTag} from "../Tag/Et2EmailTag";
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
// Stub global egw
|
||||
// @ts-ignore
|
||||
window.egw = {
|
||||
tooltipUnbind: () => {},
|
||||
lang: i => i + "*",
|
||||
image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=",
|
||||
webserverUrl: "",
|
||||
app: (_app) => _app,
|
||||
jsonq: () => Promise.resolve({})
|
||||
};
|
||||
|
||||
describe('Et2EmailTag', () =>
|
||||
{
|
||||
let component : Et2EmailTag;
|
||||
|
||||
beforeEach(async() =>
|
||||
{
|
||||
component = await fixture<Et2EmailTag>(html`
|
||||
<et2-email-tag value="test@example.com"></et2-email-tag>`);
|
||||
// Stub egw()
|
||||
// @ts-ignore
|
||||
sinon.stub(component, "egw").returns(window.egw);
|
||||
await component.updateComplete;
|
||||
|
||||
// Asserting this instanceOf forces class loading
|
||||
assert.instanceOf(component, Et2EmailTag);
|
||||
});
|
||||
|
||||
it('should be defined', () =>
|
||||
{
|
||||
assert.isDefined(component);
|
||||
});
|
||||
|
||||
it('should have a value property', () =>
|
||||
{
|
||||
assert.equal(component.value, 'test@example.com');
|
||||
});
|
||||
|
||||
it('should have a contactPlus property', () =>
|
||||
{
|
||||
assert.isTrue(component.contactPlus);
|
||||
});
|
||||
|
||||
it('should have an onlyEmail property', () =>
|
||||
{
|
||||
assert.isFalse(component.onlyEmail);
|
||||
});
|
||||
|
||||
it('should have a fullEmail property', () =>
|
||||
{
|
||||
assert.isFalse(component.fullEmail);
|
||||
});
|
||||
|
||||
it('should open addressbook with email preset on (+) click', () =>
|
||||
{
|
||||
window.egw.open = () =>
|
||||
{
|
||||
open: (url, app, mode, extra) =>
|
||||
{
|
||||
assert.equal(url, '');
|
||||
assert.equal(app, 'addressbook');
|
||||
assert.equal(mode, 'add');
|
||||
assert.equal(extra['presets[email]'], 'test@example.com');
|
||||
}
|
||||
};
|
||||
component.handleMouseDown(new MouseEvent('click'));
|
||||
});
|
||||
|
||||
it('should open addressbook CRM on avatar click', async() =>
|
||||
{
|
||||
// Fake data to test against
|
||||
const contact = {
|
||||
id: '123',
|
||||
n_fn: 'Test User',
|
||||
photo: 'test.jpg'
|
||||
};
|
||||
component.value = 'test@example.com';
|
||||
component.checkContact = async(email) => contact;
|
||||
component.egw.open = () =>
|
||||
{
|
||||
open: (id, app, mode, extra) =>
|
||||
{
|
||||
assert.equal(id, contact.id);
|
||||
assert.equal(app, 'addressbook');
|
||||
assert.equal(mode, 'view');
|
||||
assert.deepEqual(extra, {title: contact.n_fn, icon: contact.photo});
|
||||
}
|
||||
};
|
||||
await component.handleContactMouseDown(new MouseEvent('click'));
|
||||
});
|
||||
});
|
@ -25,11 +25,13 @@ async function before()
|
||||
// Create an element to test with, and wait until it's ready
|
||||
// @ts-ignore
|
||||
element = await fixture<Et2Select>(html`
|
||||
<et2-select label="I'm a select"/>
|
||||
<et2-select label="I'm a select">
|
||||
</et2-select>
|
||||
`);
|
||||
|
||||
// Stub egw()
|
||||
sinon.stub(element, "egw").returns(window.egw);
|
||||
await elementUpdated(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
@ -48,7 +50,6 @@ describe("Select widget basics", () =>
|
||||
it('has a label', async() =>
|
||||
{
|
||||
element.set_label("Label set");
|
||||
// @ts-ignore TypeScript doesn't recognize widgets as Elements
|
||||
await elementUpdated(element);
|
||||
|
||||
assert.equal(element.querySelector("[slot='label']").textContent, "Label set");
|
||||
@ -59,6 +60,36 @@ describe("Select widget basics", () =>
|
||||
assert.notExists(element.querySelector("option"), "Static option not found in DOM");
|
||||
assert.deepEqual(element.select_options, [], "Unexpected option(s)");
|
||||
})
|
||||
|
||||
it("closes when losing focus", async() =>
|
||||
{
|
||||
// WIP
|
||||
const blurSpy = sinon.spy();
|
||||
element.addEventListener('sl-hide', blurSpy);
|
||||
const showPromise = new Promise(resolve =>
|
||||
{
|
||||
element.addEventListener("sl-after-show", resolve);
|
||||
});
|
||||
const hidePromise = new Promise(resolve =>
|
||||
{
|
||||
element.addEventListener("sl-hide", resolve);
|
||||
});
|
||||
await elementUpdated(element);
|
||||
element.focus();
|
||||
|
||||
await showPromise;
|
||||
await elementUpdated(element);
|
||||
|
||||
element.blur();
|
||||
await elementUpdated(element);
|
||||
|
||||
await hidePromise;
|
||||
|
||||
sinon.assert.calledOnce(blurSpy);
|
||||
|
||||
// Check that it actually closed dropdown
|
||||
assert.isFalse(element.select?.hasAttribute("open"));
|
||||
})
|
||||
});
|
||||
|
||||
describe("Multiple", () =>
|
||||
@ -69,10 +100,11 @@ describe("Multiple", () =>
|
||||
// @ts-ignore
|
||||
element = await fixture<Et2Select>(html`
|
||||
<et2-select label="I'm a select" multiple="true">
|
||||
<sl-menu-item value="one">One</sl-menu-item>
|
||||
<sl-menu-item value="two">Two</sl-menu-item>
|
||||
<option value="one">One</option>
|
||||
<option value="two">Two</option>
|
||||
</et2-select>
|
||||
`);
|
||||
element.loadFromXML(element);
|
||||
element.set_value("one,two");
|
||||
|
||||
// Stub egw()
|
||||
@ -83,14 +115,14 @@ describe("Multiple", () =>
|
||||
|
||||
it("Can remove tags", async() =>
|
||||
{
|
||||
assert.equal(element.querySelectorAll("sl-menu-item").length, 2, "Did not find options");
|
||||
assert.equal(element.select.querySelectorAll("sl-option").length, 2, "Did not find options");
|
||||
|
||||
assert.sameMembers(element.value, ["one", "two"]);
|
||||
let tags = element.shadowRoot.querySelectorAll('.select__tags > *');
|
||||
let tags = element.select.combobox.querySelectorAll('.select__tags et2-tag');
|
||||
|
||||
// Await tags to render
|
||||
let tag_updates = []
|
||||
element.shadowRoot.querySelectorAll(element.tagTag).forEach((t : Et2Tag) => tag_updates.push(t.updateComplete));
|
||||
element.select.combobox.querySelectorAll("et2-tag").forEach((t : Et2Tag) => tag_updates.push(t.updateComplete));
|
||||
await Promise.all(tag_updates);
|
||||
|
||||
assert.equal(tags.length, 2);
|
||||
@ -110,15 +142,21 @@ describe("Multiple", () =>
|
||||
// Wait for widget to update
|
||||
await element.updateComplete;
|
||||
tag_updates = []
|
||||
element.shadowRoot.querySelectorAll(element.tagTag).forEach((t : Et2Tag) => tag_updates.push(t.updateComplete));
|
||||
element.select.combobox.querySelectorAll('et2-tag').forEach((t : Et2Tag) => tag_updates.push(t.updateComplete));
|
||||
await Promise.all(tag_updates);
|
||||
|
||||
// Check
|
||||
assert.sameMembers(element.value, ["two"], "Removing tag did not remove value");
|
||||
tags = element.shadowRoot.querySelectorAll('.select__tags > *');
|
||||
tags = element.select.combobox.querySelectorAll('.select__tags et2-tag');
|
||||
assert.equal(tags.length, 1, "Removed tag is still there");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
inputBasicTests(before, "", "select");
|
||||
inputBasicTests(async() =>
|
||||
{
|
||||
const element = await before();
|
||||
element.noLang = true;
|
||||
element.select_options = [{value: "", label: ""}];
|
||||
return element
|
||||
}, "", "sl-select");
|
@ -1,8 +1,11 @@
|
||||
import {assert, elementUpdated, fixture, html} from '@open-wc/testing';
|
||||
import {Et2Box} from "../../Layout/Et2Box/Et2Box";
|
||||
import {Et2Select, SelectOption} from "../Et2Select";
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import * as sinon from "sinon";
|
||||
import {et2_arrayMgr} from "../../et2_core_arrayMgr";
|
||||
import {SelectOption} from "../FindSelectOptions";
|
||||
import '../Select/Et2SelectNumber';
|
||||
import {Et2SelectNumber} from "../Select/Et2SelectNumber";
|
||||
|
||||
let parser = new window.DOMParser();
|
||||
|
||||
@ -30,7 +33,6 @@ describe("Select widget", () =>
|
||||
beforeEach(async() =>
|
||||
{
|
||||
// This stuff because otherwise Et2Select isn't actually loaded when testing
|
||||
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||
element = await fixture<Et2Select>(html`
|
||||
<et2-select></et2-select>
|
||||
`);
|
||||
@ -39,7 +41,6 @@ describe("Select widget", () =>
|
||||
assert.instanceOf(element, Et2Select);
|
||||
element.remove();
|
||||
|
||||
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||
container = await fixture<Et2Box>(html`
|
||||
<et2-box/>
|
||||
`);
|
||||
@ -51,7 +52,7 @@ describe("Select widget", () =>
|
||||
|
||||
describe("Finds options", () =>
|
||||
{
|
||||
it("static", async() =>
|
||||
it("from DOM/Template", async() =>
|
||||
{
|
||||
/** SETUP **/
|
||||
// Create an element to test with, and wait until it's ready
|
||||
@ -60,13 +61,12 @@ describe("Select widget", () =>
|
||||
container.loadFromXML(parser.parseFromString(node, "text/xml"));
|
||||
|
||||
// wait for asychronous changes to the DOM
|
||||
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||
await elementUpdated(container);
|
||||
element = <Et2Select>container.getWidgetById('select');
|
||||
await element.updateComplete;
|
||||
|
||||
/** TESTING **/
|
||||
assert.isNotNull(element.querySelector("[value='option']"), "Missing static option");
|
||||
assert.isNotNull(element.select.querySelector("[value='option']"), "Missing template option");
|
||||
});
|
||||
|
||||
it("directly in sel_options", async() =>
|
||||
@ -80,16 +80,15 @@ describe("Select widget", () =>
|
||||
container.loadFromXML(parser.parseFromString(node, "text/xml"));
|
||||
|
||||
// wait for asychronous changes to the DOM
|
||||
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||
await elementUpdated(container);
|
||||
element = <Et2Select>container.getWidgetById('select');
|
||||
await element.updateComplete;
|
||||
|
||||
/** TESTING **/
|
||||
assert.equal(element.querySelectorAll("sl-menu-item").length, 2);
|
||||
assert.equal(element.select.querySelectorAll("sl-option").length, 2);
|
||||
});
|
||||
|
||||
it("merges static options with sel_options", async() =>
|
||||
it("merges template options with sel_options", async() =>
|
||||
{
|
||||
/** SETUP **/
|
||||
|
||||
@ -101,19 +100,115 @@ describe("Select widget", () =>
|
||||
container.loadFromXML(parser.parseFromString(node, "text/xml"));
|
||||
|
||||
// wait for asychronous changes to the DOM
|
||||
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||
await elementUpdated(container);
|
||||
element = <Et2Select>container.getWidgetById('select');
|
||||
await element.updateComplete;
|
||||
|
||||
/** TESTING **/
|
||||
|
||||
// @ts-ignore o.value isn't known by TypeScript, but it's there
|
||||
let option_keys = Object.values(element.querySelectorAll("sl-menu-item")).map(o => o.value);
|
||||
assert.include(option_keys, "option", "Static option missing");
|
||||
let option_keys = Object.values(element.select.querySelectorAll("sl-option")).map(o => o.value);
|
||||
assert.include(option_keys, "option", "Template option missing");
|
||||
assert.includeMembers(option_keys, ["1", "2", "option"], "Option mis-match");
|
||||
assert.equal(option_keys.length, 3);
|
||||
});
|
||||
|
||||
it("static options (number)", async() =>
|
||||
{
|
||||
/** SETUP **/
|
||||
// Create an element to test with, and wait until it's ready
|
||||
// Default number options are 1-10
|
||||
let element = await fixture<Et2SelectNumber>(html`
|
||||
<et2-select-number></et2-select-number>
|
||||
`);
|
||||
|
||||
// wait for asychronous changes to the DOM
|
||||
await elementUpdated(element);
|
||||
await element.updateComplete;
|
||||
|
||||
/** TESTING **/
|
||||
assert.equal(element.select.querySelectorAll("sl-option").length, 10);
|
||||
});
|
||||
|
||||
it("merges static options with sel_options", async() =>
|
||||
{
|
||||
/** SETUP **/
|
||||
let options = [
|
||||
<SelectOption>{value: "one", label: "Option 1"},
|
||||
<SelectOption>{value: "two", label: "Option 2"}
|
||||
];
|
||||
// Create an element to test with, and wait until it's ready
|
||||
let node = '<et2-select-number id="select" label="I am a select" max="2"></et2-select-number>';
|
||||
container.setArrayMgr("sel_options", new et2_arrayMgr({
|
||||
select: options
|
||||
}));
|
||||
container.loadFromXML(parser.parseFromString(node, "text/xml"));
|
||||
|
||||
// wait for asychronous changes to the DOM
|
||||
await elementUpdated(container);
|
||||
element = <Et2Select>container.getWidgetById('select');
|
||||
await element.updateComplete;
|
||||
|
||||
/** TESTING **/
|
||||
let option_keys = Object.values(element.select.querySelectorAll("sl-option")).map(o => o.value);
|
||||
assert.includeMembers(option_keys, ["1", "2", "one", "two"], "Option mis-match");
|
||||
assert.equal(option_keys.length, 4);
|
||||
});
|
||||
|
||||
it("merges static options with template options", async() =>
|
||||
{
|
||||
/** SETUP **/
|
||||
|
||||
// Create an element to test with, and wait until it's ready
|
||||
// Default number options are 1-10
|
||||
let element = await fixture<Et2SelectNumber>(html`
|
||||
<et2-select-number>
|
||||
<option value="option">option label</option>
|
||||
</et2-select-number>
|
||||
`);
|
||||
|
||||
// wait for asychronous changes to the DOM
|
||||
element.loadFromXML(element);
|
||||
await elementUpdated(element);
|
||||
await element.updateComplete;
|
||||
|
||||
/** TESTING **/
|
||||
let option_keys = Object.values(element.select.querySelectorAll("sl-option")).map(o => o.value);
|
||||
assert.include(option_keys, "option", "Template option missing");
|
||||
assert.includeMembers(option_keys, ["1", "2", "option"], "Option mis-match");
|
||||
assert.equal(option_keys.length, 11);
|
||||
});
|
||||
|
||||
it("actually shows the options", async() =>
|
||||
{
|
||||
// Create an element to test with, and wait until it's ready
|
||||
// @ts-ignore
|
||||
element = await fixture<Et2Select>(html`
|
||||
<et2-select label="I'm a select"></et2-select>
|
||||
`);
|
||||
// Stub egw()
|
||||
sinon.stub(element, "egw").returns(window.egw);
|
||||
element.select_options = [
|
||||
{value: "one", label: "one"},
|
||||
{value: "two", label: "two"},
|
||||
{value: "three", label: "three"},
|
||||
{value: "four", label: "four"},
|
||||
{value: "five", label: "five"}
|
||||
];
|
||||
|
||||
await element.updateComplete;
|
||||
await elementUpdated(element);
|
||||
|
||||
await element.show();
|
||||
|
||||
// Not actually testing if the browser renders, just if they show where expected
|
||||
const options = element.select.querySelectorAll("sl-option");
|
||||
assert.equal(options.length, 5, "Wrong number of options");
|
||||
|
||||
// Still not checking if they're _really_ visible, just that they have the correct display
|
||||
options.forEach(o =>
|
||||
{
|
||||
assert.equal(getComputedStyle(o).display, "block", "Wrong style.display");
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe("Value tests", () =>
|
||||
|
@ -3,19 +3,22 @@
|
||||
* Currently just checking to make sure onchange is only called once.
|
||||
*/
|
||||
import {SelectOption} from "../FindSelectOptions";
|
||||
import {assert, elementUpdated, fixture, html} from '@open-wc/testing';
|
||||
import {assert, elementUpdated, fixture, html, oneEvent} from '@open-wc/testing';
|
||||
import * as sinon from 'sinon';
|
||||
import {Et2Box} from "../../Layout/Et2Box/Et2Box";
|
||||
import {Et2Select} from "../Et2Select";
|
||||
import {Et2Textbox} from "../../Et2Textbox/Et2Textbox";
|
||||
|
||||
let keep_import : Et2Textbox = new Et2Textbox();
|
||||
let keep_import : Et2Textbox = null;
|
||||
|
||||
// Stub global egw for cssImage to find
|
||||
// @ts-ignore
|
||||
window.egw = {
|
||||
ajaxUrl: url => url,
|
||||
decodePath: url => url,
|
||||
//image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=",
|
||||
lang: i => i + "*",
|
||||
link: l => l,
|
||||
tooltipUnbind: () => {},
|
||||
webserverUrl: "",
|
||||
window: window
|
||||
@ -65,14 +68,17 @@ describe("Search actions", () =>
|
||||
'</et2-select>';
|
||||
|
||||
container.loadFromXML(parser.parseFromString(node, "text/xml"));
|
||||
await elementUpdated(container);
|
||||
|
||||
const change = sinon.spy();
|
||||
let element = <Et2Select>container.getWidgetById('select');
|
||||
element.onchange = change;
|
||||
|
||||
await elementUpdated(element);
|
||||
|
||||
element.value = "two";
|
||||
const option = element.select.querySelector("[value='two']");
|
||||
const listener = oneEvent(option, "mouseup");
|
||||
option.dispatchEvent(new Event("mouseup", {bubbles: true}));
|
||||
await listener;
|
||||
|
||||
await elementUpdated(element);
|
||||
|
||||
@ -96,14 +102,14 @@ describe("Trigger search", () =>
|
||||
// Create an element to test with, and wait until it's ready
|
||||
// @ts-ignore
|
||||
element = await fixture<Et2Select>(html`
|
||||
<et2-select label="I'm a select" search=true>
|
||||
<sl-menu-item value="one">One</sl-menu-item>
|
||||
<sl-menu-item value="two">Two</sl-menu-item>
|
||||
<sl-menu-item value="three">Three</sl-menu-item>
|
||||
<sl-menu-item value="four">Four</sl-menu-item>
|
||||
<sl-menu-item value="five">Five</sl-menu-item>
|
||||
<sl-menu-item value="six">Six</sl-menu-item>
|
||||
<sl-menu-item value="seven">Seven</sl-menu-item>
|
||||
<et2-select label="I'm a select" search>
|
||||
<sl-option value="one">One</sl-option>
|
||||
<sl-option value="two">Two</sl-option>
|
||||
<sl-option value="three">Three</sl-option>
|
||||
<sl-option value="four">Four</sl-option>
|
||||
<sl-option value="five">Five</sl-option>
|
||||
<sl-option value="six">Six</sl-option>
|
||||
<sl-option value="seven">Seven</sl-option>
|
||||
</et2-select>
|
||||
`);
|
||||
// Stub egw()
|
||||
@ -111,6 +117,7 @@ describe("Trigger search", () =>
|
||||
|
||||
await element.updateComplete;
|
||||
await element._searchInputNode.updateComplete;
|
||||
await elementUpdated(element);
|
||||
});
|
||||
|
||||
afterEach(() =>
|
||||
@ -125,11 +132,11 @@ describe("Trigger search", () =>
|
||||
let searchSpy = sinon.spy(element, "startSearch");
|
||||
|
||||
// Send two keypresses, but we need to explicitly set the value
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "o"}));
|
||||
element._searchInputNode.value = "o";
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "o"}));
|
||||
assert(searchSpy.notCalled);
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "n"}));
|
||||
element._searchInputNode.value = "on";
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "n"}));
|
||||
assert(searchSpy.notCalled);
|
||||
|
||||
// Skip the timeout
|
||||
@ -145,8 +152,8 @@ describe("Trigger search", () =>
|
||||
let searchSpy = sinon.spy(element, "startSearch");
|
||||
|
||||
// Send two keypresses, but we need to explicitly set the value
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "o"}));
|
||||
element._searchInputNode.value = "t";
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "o"}));
|
||||
assert(searchSpy.notCalled);
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "Enter"}));
|
||||
|
||||
@ -161,11 +168,207 @@ describe("Trigger search", () =>
|
||||
let searchSpy = sinon.spy(element, "startSearch");
|
||||
|
||||
// Send two keypresses, but we need to explicitly set the value
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "t"}));
|
||||
element._searchInputNode.value = "t";
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "t"}));
|
||||
element._searchInputNode.dispatchEvent(new KeyboardEvent("keydown", {"key": "Escape"}));
|
||||
|
||||
assert(searchSpy.notCalled, "startSearch() was called");
|
||||
assert(abortSpy.calledOnce, "_handleSearchAbort() was not called");
|
||||
})
|
||||
});
|
||||
|
||||
async function doSearch(element, search)
|
||||
{
|
||||
// we need to explicitly set the value
|
||||
element._searchInputNode.value = search;
|
||||
|
||||
await element.startSearch();
|
||||
|
||||
await elementUpdated(element)
|
||||
};
|
||||
|
||||
describe("Search results", () =>
|
||||
{
|
||||
let element : Et2Select;
|
||||
const remote_results = [
|
||||
{value: "remote_one", label: "remote_one"},
|
||||
{value: "remote_two", label: "remote_two"}
|
||||
];
|
||||
let clickOption = (value) =>
|
||||
{
|
||||
const option = element.select.querySelector("[value='" + value + "']");
|
||||
let listener = oneEvent(option, "mouseup");
|
||||
option.dispatchEvent(new Event("mouseup", {bubbles: true}));
|
||||
return listener;
|
||||
}
|
||||
|
||||
// Setup run before each test
|
||||
beforeEach(async() =>
|
||||
{
|
||||
// Create an element to test with, and wait until it's ready
|
||||
// @ts-ignore
|
||||
element = await fixture<Et2Select>(html`
|
||||
<et2-select label="I'm a select" search>
|
||||
<option value="one">One</option>
|
||||
<option value="two">Two</option>
|
||||
<option value="three">Three</option>
|
||||
<option value="four">Four</option>
|
||||
<option value="five">Five</option>
|
||||
<option value="six">Six</option>
|
||||
<option value="seven">Seven</option>
|
||||
</et2-select>
|
||||
`);
|
||||
element.loadFromXML(element);
|
||||
// Stub egw()
|
||||
sinon.stub(element, "egw").returns(window.egw);
|
||||
|
||||
await element.updateComplete;
|
||||
await element._searchInputNode.updateComplete;
|
||||
await elementUpdated(element);
|
||||
});
|
||||
|
||||
it("Correct local results", async() =>
|
||||
{
|
||||
// Search
|
||||
await doSearch(element, "one")
|
||||
// Check the result is offered
|
||||
const option = element.select.querySelector("[value='one']")
|
||||
assert.isNotNull(option, "Did not find option in result");
|
||||
|
||||
// _only_ that one?
|
||||
assert.sameMembers(Array.from(element.select.querySelectorAll("sl-option")).map(e => e.value), ["one"], "Unexpected search results");
|
||||
});
|
||||
it("Correct remote results", async() =>
|
||||
{
|
||||
// Enable searching
|
||||
element.searchUrl = "test";
|
||||
|
||||
// Fake remote search
|
||||
window.egw.request = sinon.fake
|
||||
.returns(Promise.resolve([remote_results[0]]));
|
||||
|
||||
// Search
|
||||
await doSearch(element, "remote_one")
|
||||
|
||||
// Check the result is offered
|
||||
const option = element.select.querySelector("[value='remote_one']")
|
||||
assert.isNotNull(option, "Did not find option in result");
|
||||
|
||||
// _only_ that one?
|
||||
// N.B. that "one" will stay, since that's the current value
|
||||
assert.sameMembers(Array.from(element.select.querySelectorAll("sl-option.remote")).map(e => e.value), ["remote_one"], "Unexpected search results");
|
||||
});
|
||||
it("Correct local and remote together", async() =>
|
||||
{
|
||||
// Enable searching
|
||||
element.searchUrl = "test";
|
||||
|
||||
// Fake remote search
|
||||
window.egw.request = sinon.fake
|
||||
.returns(Promise.resolve([remote_results[0]]));
|
||||
|
||||
// Search
|
||||
await doSearch(element, "one")
|
||||
|
||||
// Check the result is offered
|
||||
const local_option = element.select.querySelector("[value='one']")
|
||||
assert.isNotNull(local_option, "Did not find local option in result");
|
||||
const remote_option = element.select.querySelector("[value='remote_one']")
|
||||
assert.isNotNull(remote_option, "Did not find remote option in result");
|
||||
|
||||
// _only_ that one?
|
||||
assert.sameMembers(Array.from(element.select.querySelectorAll("sl-option")).map(e => e.value), ["one", "remote_one"], "Unexpected search results");
|
||||
});
|
||||
it("Selected local result is in value", async() =>
|
||||
{
|
||||
// Search
|
||||
await doSearch(element, "one")
|
||||
|
||||
// "Click" that one
|
||||
await clickOption("one");
|
||||
await element.updateComplete;
|
||||
|
||||
assert.equal(element.value, "one", "Selected search result was not in value");
|
||||
});
|
||||
it("Selected remote result in value", async() =>
|
||||
{
|
||||
// Enable searching
|
||||
element.searchUrl = "test";
|
||||
|
||||
// Fake remote search
|
||||
window.egw.request = sinon.fake
|
||||
.returns(Promise.resolve([remote_results[0]]));
|
||||
|
||||
// Search
|
||||
await doSearch(element, "remote_one")
|
||||
|
||||
// Click
|
||||
await clickOption("remote_one");
|
||||
await element.updateComplete;
|
||||
|
||||
assert.equal(element.value, "remote_one", "Selected search result was not in value");
|
||||
});
|
||||
it("Selected multiple remote results in value", async() =>
|
||||
{
|
||||
// Enable multiple
|
||||
element.multiple = true;
|
||||
|
||||
// Clear auto-selected value
|
||||
element.value = "";
|
||||
|
||||
// Enable searching
|
||||
element.searchUrl = "test";
|
||||
|
||||
// Fake remote search
|
||||
window.egw.request = sinon.fake
|
||||
.returns(Promise.resolve(remote_results));
|
||||
|
||||
// Search
|
||||
await doSearch(element, "doesn't matter, we're faking it")
|
||||
|
||||
// Click
|
||||
const values = ["remote_one", "remote_two"];
|
||||
let listener;
|
||||
values.forEach(value =>
|
||||
{
|
||||
listener = clickOption(value);
|
||||
});
|
||||
await listener;
|
||||
await element.updateComplete;
|
||||
|
||||
assert.deepEqual(element.value, values, "Selected search results were not in value");
|
||||
});
|
||||
it("Adding (multiple) remote keeps value", async() =>
|
||||
{
|
||||
const values = ["remote_one", "remote_two"];
|
||||
|
||||
// Enable multiple
|
||||
element.multiple = true;
|
||||
|
||||
// Clear value ("one" was selected automatically)
|
||||
element.value = "";
|
||||
await element.updateComplete;
|
||||
|
||||
// Enable searching
|
||||
element.searchUrl = "test";
|
||||
|
||||
// Fake remote search
|
||||
window.egw.request = sinon.fake
|
||||
.returns(Promise.resolve(remote_results));
|
||||
|
||||
// Search
|
||||
await doSearch(element, "doesn't matter, we're faking it")
|
||||
|
||||
debugger;
|
||||
// Select the first one
|
||||
await clickOption("remote_one");
|
||||
await element.updateComplete;
|
||||
|
||||
// Search & select another one
|
||||
await doSearch(element, "doesn't matter, we're faking it");
|
||||
await clickOption("remote_two");
|
||||
await element.updateComplete;
|
||||
|
||||
assert.deepEqual(element.value, values, "Selected search results were not in value");
|
||||
});
|
||||
});
|
@ -11,7 +11,7 @@
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {SlSpinner} from "@shoelace-style/shoelace";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
|
||||
export class Et2Spinner extends Et2Widget(SlSpinner)
|
||||
{
|
||||
|
@ -8,8 +8,8 @@
|
||||
* @author Hadi Nategh
|
||||
*/
|
||||
|
||||
|
||||
import {css, html, SlotMixin} from "@lion/core";
|
||||
import {css, html} from "lit";
|
||||
import {SlotMixin} from "@lion/core";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import '../Et2Image/Et2Image';
|
||||
import {SlSwitch} from "@shoelace-style/shoelace";
|
||||
@ -151,14 +151,13 @@ export class Et2Switch extends Et2InputWidget(SlotMixin(SlSwitch))
|
||||
if(new_value)
|
||||
{
|
||||
this._labelNode?.classList.add('on');
|
||||
this.checked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._labelNode?.classList.remove('on');
|
||||
this.checked = false;
|
||||
}
|
||||
}
|
||||
this.checked = !!new_value;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -174,8 +173,9 @@ export class Et2Switch extends Et2InputWidget(SlotMixin(SlSwitch))
|
||||
|
||||
labelTemplate()
|
||||
{
|
||||
const labelClass = this.checked ? "label on" : "label";
|
||||
return html`
|
||||
<span class="label" aria-label="${this.label}">
|
||||
<span class=${labelClass} aria-label="${this.label}">
|
||||
<span class="on">${this.toggleOn}</span>
|
||||
<span class="off">${this.toggleOff}</span>
|
||||
</span>
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {SlTextarea} from "@shoelace-style/shoelace";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
@ -29,14 +29,16 @@ export class Et2Textarea extends Et2InputWidget(SlTextarea)
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.textarea--resize-vertical .textarea__control {
|
||||
|
||||
.textarea--resize-vertical {
|
||||
height: 100%;
|
||||
}
|
||||
:host::part(form-control) {
|
||||
height: 100%;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
:host::part(base) {
|
||||
|
||||
:host::part(form-control-input), :host::part(textarea) {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import {Et2Textbox} from "./Et2Textbox";
|
||||
import {css, html, render} from "@lion/core";
|
||||
import {css, html, render} from "lit";
|
||||
|
||||
export class Et2Number extends Et2Textbox
|
||||
{
|
||||
|
@ -11,7 +11,9 @@
|
||||
import {Et2InvokerMixin} from "../Et2Url/Et2InvokerMixin";
|
||||
import {Et2Textbox} from "./Et2Textbox";
|
||||
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
||||
import {classMap, html, ifDefined} from "@lion/core";
|
||||
import {html} from "lit";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import {ifDefined} from "lit/directives/if-defined.js";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
|
||||
const isChromium = navigator.userAgentData?.brands.some(b => b.brand.includes('Chromium'));
|
||||
@ -68,14 +70,14 @@ export class Et2Password extends Et2InvokerMixin(Et2Textbox)
|
||||
|
||||
if(typeof attrs.viewable !== "undefined")
|
||||
{
|
||||
attrs['passwordToggle'] = attrs.viewable;
|
||||
attrs['togglePassword'] = attrs.viewable;
|
||||
delete attrs.viewable;
|
||||
}
|
||||
if(typeof attrs.passwordToggle !== "undefined" && !attrs.passwordToggle
|
||||
|| typeof attrs.passwordToggle == "string" && !this.getArrayMgr("content").parseBoolExpression(attrs.passwordToggle))
|
||||
if(typeof attrs.togglePassword !== "undefined" && !attrs.togglePassword
|
||||
|| typeof attrs.togglePassword == "string" && !this.getArrayMgr("content").parseBoolExpression(attrs.togglePassword))
|
||||
{
|
||||
// Unset passwordToggle if its false. It's from parent, and it doesn't handle string "false" = false
|
||||
delete attrs.passwordToggle;
|
||||
// Unset togglePassword if its false. It's from parent, and it doesn't handle string "false" = false
|
||||
delete attrs.togglePassword;
|
||||
}
|
||||
|
||||
super.transformAttributes(attrs);
|
||||
@ -299,7 +301,7 @@ export class Et2Password extends Et2InvokerMixin(Et2Textbox)
|
||||
: ''
|
||||
}
|
||||
${
|
||||
this.passwordToggle && !this.disabled
|
||||
this.togglePassword && !this.disabled
|
||||
? html`
|
||||
<button
|
||||
part="password-toggle-button"
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, PropertyValues} from "@lion/core";
|
||||
import {css, PropertyValues} from "lit";
|
||||
import {Regex} from "../Validators/Regex";
|
||||
import {SlInput} from "@shoelace-style/shoelace";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
|
352
api/js/etemplate/Et2TreeWidget/Et2Tree.ts
Normal file
352
api/js/etemplate/Et2TreeWidget/Et2Tree.ts
Normal file
@ -0,0 +1,352 @@
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {SlTree} from "@shoelace-style/shoelace";
|
||||
import {Et2Link} from "../Et2Link/Et2Link";
|
||||
import {et2_no_init} from "../et2_core_common";
|
||||
import {egw, framework} from "../../jsapi/egw_global";
|
||||
import {SelectOption, find_select_options, cleanSelectOptions} from "../Et2Select/FindSelectOptions";
|
||||
import {egwIsMobile} from "../../egw_action/egw_action_common";
|
||||
import {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin";
|
||||
import {LitElement, css, TemplateResult, html} from "lit";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
|
||||
export type TreeItem = {
|
||||
child: Boolean | 1,
|
||||
data?: Object,//{sieve:true,...} or {acl:true} or other
|
||||
id: String,
|
||||
im0: String,
|
||||
im1: String,
|
||||
im2: String,
|
||||
item: TreeItem[],
|
||||
checked?: Boolean,
|
||||
nocheckbox: number | Boolean,
|
||||
open: 0 | 1,
|
||||
parent: String,
|
||||
text: String,
|
||||
tooltip: String
|
||||
}
|
||||
|
||||
|
||||
export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
|
||||
return [
|
||||
shoelace,
|
||||
// @ts-ignore
|
||||
...super.styles,
|
||||
css`
|
||||
|
||||
`
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
private currentItem: TreeItem
|
||||
private input: any = null;
|
||||
private autoloading_url: any;
|
||||
private selectOptions: TreeItem[] = [];
|
||||
private needsLazyLoading: Boolean = true;
|
||||
/**
|
||||
* Limit server searches to 100 results, matches Link::DEFAULT_NUM_ROWS
|
||||
* @type {number}
|
||||
*/
|
||||
static RESULT_LIMIT: number = 100;
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
public loadFromXML()
|
||||
{
|
||||
//if(this.id)
|
||||
this.selectOptions = <TreeItem[]><unknown>find_select_options(this)[1]
|
||||
}
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
multiple: {
|
||||
name: "",
|
||||
type: Boolean,
|
||||
default: false,
|
||||
description: "Allow selecting multiple options"
|
||||
},
|
||||
selectOptions: {
|
||||
type: "any",
|
||||
name: "Select options",
|
||||
default: {},
|
||||
description: "Used to set the tree options."
|
||||
},
|
||||
onClick: {
|
||||
name: "onClick",
|
||||
type: "js",
|
||||
description: "JS code which gets executed when clicks on text of a node"
|
||||
},
|
||||
onSelect: {
|
||||
name: "onSelect",
|
||||
type: "js",
|
||||
default: et2_no_init,
|
||||
description: "Javascript executed when user selects a node"
|
||||
},
|
||||
onCheck: {
|
||||
name: "onCheck",
|
||||
type: "js",
|
||||
default: et2_no_init,
|
||||
description: "Javascript executed when user checks a node"
|
||||
},
|
||||
// TODO do this : --> onChange event is mapped depending on multiple to onCheck or onSelect
|
||||
onOpenStart: {
|
||||
name: "onOpenStart",
|
||||
type: "js",
|
||||
default: et2_no_init,
|
||||
description: "Javascript function executed when user opens a node: function(_id, _widget, _hasChildren) returning true to allow opening!"
|
||||
},
|
||||
onOpenEnd: {
|
||||
name: "onOpenEnd",
|
||||
type: "js",
|
||||
default: et2_no_init,
|
||||
description: "Javascript function executed when opening a node is finished: function(_id, _widget, _hasChildren)"
|
||||
},
|
||||
imagePath: {
|
||||
name: "Image directory",
|
||||
type: String,
|
||||
default: egw().webserverUrl + "/api/templates/default/images/dhtmlxtree/",//TODO we will need a different path here! maybe just rename the path?
|
||||
description: "Directory for tree structure images, set on server-side to 'dhtmlx' subdir of templates image-directory"
|
||||
},
|
||||
value: {
|
||||
type: "any",
|
||||
default: {}
|
||||
},
|
||||
actions: {
|
||||
name: "Actions array",
|
||||
type: "any",
|
||||
default: et2_no_init,
|
||||
description: "List of egw actions that can be done on the tree. This includes context menu, drag and drop. TODO: Link to action documentation"
|
||||
},
|
||||
autoLoading: {
|
||||
name: "Auto loading",
|
||||
type: String,
|
||||
default: "",
|
||||
description: "JSON URL or menuaction to be called for nodes marked with child=1, but not having children, GET parameter selected contains node-id"
|
||||
},
|
||||
stdImages: {
|
||||
name: "Standard images",
|
||||
type: String,
|
||||
default: "",
|
||||
description: "comma-separated names of icons for a leaf, closed and opened folder (default: leaf.png,folderClosed.png,folderOpen.png), images with extension get loaded from imagePath, just 'image' or 'appname/image' are allowed too"
|
||||
},
|
||||
multiMarking: {
|
||||
name: "multi marking",
|
||||
type: "any",
|
||||
default: false,
|
||||
description: "Allow marking multiple nodes, default is false which means disabled multiselection, true or 'strict' activates it and 'strict' makes it strict to only same level marking"
|
||||
},
|
||||
highlighting: {
|
||||
name: "highlighting",
|
||||
type: Boolean,
|
||||
default: false,
|
||||
description: "Add highlighting class on hovered over item, highlighting is disabled by default"
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
public set onOpenStart(_handler: Function)
|
||||
{
|
||||
this.installHandler("onOpenStart", _handler)
|
||||
}
|
||||
|
||||
public set onChange(_handler: Function)
|
||||
{
|
||||
this.installHandler("onChange", _handler)
|
||||
}
|
||||
|
||||
public set onClick(_handler: Function)
|
||||
{
|
||||
this.installHandler("onClick", _handler)
|
||||
}
|
||||
|
||||
public set onSelect(_handler: Function)
|
||||
{
|
||||
this.installHandler("onSelect", _handler)
|
||||
}
|
||||
|
||||
public set onOpenEnd(_handler: Function)
|
||||
{
|
||||
this.installHandler("onOpenEnd", _handler)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated assign to onOpenStart
|
||||
* @param _handler
|
||||
*/
|
||||
public set_onopenstart(_handler: Function)
|
||||
{
|
||||
this.installHandler("onOpenStart", _handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated assign to onChange
|
||||
* @param _handler
|
||||
*/
|
||||
public set_onchange(_handler: Function)
|
||||
{
|
||||
this.installHandler('onchange', _handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated assign to onClick
|
||||
* @param _handler
|
||||
*/
|
||||
public set_onclick(_handler: Function)
|
||||
{
|
||||
this.installHandler('onclick', _handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated assign to onSelect
|
||||
* @param _handler
|
||||
*/
|
||||
public set_onselect(_handler: Function)
|
||||
{
|
||||
this.installHandler('onselect', _handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated assign to onOpenEnd
|
||||
* @param _handler
|
||||
*/
|
||||
public set_onopenend(_handler: Function)
|
||||
{
|
||||
this.installHandler('onOpenEnd', _handler);
|
||||
}
|
||||
|
||||
|
||||
private installHandler(_name: String, _handler: Function)
|
||||
{
|
||||
if (this.input == null) this.createTree();
|
||||
// automatic convert onChange event to oncheck or onSelect depending on multiple is used or not
|
||||
// if (_name == "onchange") {
|
||||
// _name = this.options.multiple ? "oncheck" : "onselect"
|
||||
// }
|
||||
// let handler = _handler;
|
||||
// let widget = this;
|
||||
// this.input.attachEvent(_name, function(_id){
|
||||
// let args = jQuery.makeArray(arguments);
|
||||
// // splice in widget as 2. parameter, 1. is new node-id, now 3. is old node id
|
||||
// args.splice(1, 0, widget);
|
||||
// // try to close mobile sidemenu after clicking on node
|
||||
// if (egwIsMobile() && typeof args[2] == 'string') framework.toggleMenu('on');
|
||||
// return handler.apply(this, args);
|
||||
// });
|
||||
}
|
||||
|
||||
private createTree()
|
||||
{
|
||||
// widget.input = document.querySelector("et2-tree");
|
||||
// // Allow controlling icon size by CSS
|
||||
// widget.input.def_img_x = "";
|
||||
// widget.input.def_img_y = "";
|
||||
//
|
||||
// // to allow "," in value, eg. folder-names, IF value is specified as array
|
||||
// widget.input.dlmtr = ':}-*(';
|
||||
// @ts-ignore from static get properties
|
||||
if (this.autoLoading)
|
||||
{
|
||||
// @ts-ignore from static get properties
|
||||
let url = this.autoLoading;
|
||||
|
||||
if (url.charAt(0) != '/' && url.substr(0, 4) != 'http')
|
||||
{
|
||||
url = '/json.php?menuaction=' + url;
|
||||
}
|
||||
this.autoloading_url = url;
|
||||
}
|
||||
}
|
||||
|
||||
private handleLazyLoading(_item: TreeItem)
|
||||
{
|
||||
let sendOptions = {
|
||||
num_rows: Et2Tree.RESULT_LIMIT,
|
||||
}
|
||||
return egw().request(egw().link(egw().ajaxUrl(egw().decodePath(this.autoloading_url)),
|
||||
{
|
||||
id: _item.id
|
||||
}), [sendOptions])
|
||||
.then((results) => {
|
||||
|
||||
// If results have a total included, pull it out.
|
||||
// It will cause errors if left in the results
|
||||
// this._total_result_count = results.length;
|
||||
// if(typeof results.total !== "undefined")
|
||||
// {
|
||||
// this._total_result_count = results.total;
|
||||
// delete results.total;
|
||||
// }
|
||||
// let entries = cleanSelectOptions(results);
|
||||
// this.processRemoteResults(entries);
|
||||
// return entries;
|
||||
return results
|
||||
});
|
||||
}
|
||||
|
||||
//this.selectOptions = find_select_options(this)[1];
|
||||
_optionTemplate(selectOption: TreeItem)
|
||||
{
|
||||
// @ts-ignore
|
||||
|
||||
//slot = expanded/collapsed instead of expand/collapse like it is in documentation
|
||||
//selectOption.child === 1
|
||||
return html`
|
||||
<sl-tree-item
|
||||
.currentItem=${selectOption}
|
||||
?lazy=${this.needsLazyLoading}
|
||||
@sl-lazy-load=${() => this.handleLazyLoading(selectOption)}
|
||||
>
|
||||
${selectOption.text}
|
||||
${repeat(selectOption.item, this._optionTemplate.bind(this))}
|
||||
</sl-tree-item>`
|
||||
}
|
||||
|
||||
public render(): unknown
|
||||
{
|
||||
return html`
|
||||
<sl-tree
|
||||
|
||||
>
|
||||
${repeat(this.selectOptions, this._optionTemplate.bind(this))}
|
||||
</sl-tree>
|
||||
`;
|
||||
}
|
||||
|
||||
protected remoteQuery(search: string, options: object): Promise<SelectOption[]>
|
||||
{
|
||||
// Include a limit, even if options don't, to avoid massive lists breaking the UI
|
||||
let sendOptions = {
|
||||
num_rows: Et2Tree.RESULT_LIMIT,
|
||||
...options
|
||||
}
|
||||
return this.egw().request(this.egw().link(this.egw().ajaxUrl(this.egw().decodePath(this.searchUrl)),
|
||||
{query: search, ...sendOptions}), [search, sendOptions]).then((results) => {
|
||||
// If results have a total included, pull it out.
|
||||
// It will cause errors if left in the results
|
||||
this._total_result_count = results.length;
|
||||
if (typeof results.total !== "undefined")
|
||||
{
|
||||
this._total_result_count = results.total;
|
||||
delete results.total;
|
||||
}
|
||||
let entries = cleanSelectOptions(results);
|
||||
this.processRemoteResults(entries);
|
||||
return entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("et2-tree", Et2Tree);
|
||||
|
||||
|
12
api/js/etemplate/Et2TreeWidget/MailTree.ts
Normal file
12
api/js/etemplate/Et2TreeWidget/MailTree.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {Et2Tree} from "./Et2Tree";
|
||||
|
||||
export function initMailTree(): Et2Tree {
|
||||
const changeFunction = () => {
|
||||
console.log("change"+tree)
|
||||
}
|
||||
const tree: Et2Tree = document.querySelector("et2-tree");
|
||||
tree.selection = "single";
|
||||
tree.addEventListener("sl-selection-change", (event)=>{console.log(event)})
|
||||
return tree;
|
||||
}
|
||||
|
7
api/js/etemplate/Et2TreeWidget/Tests/TreeTest.js
Normal file
7
api/js/etemplate/Et2TreeWidget/Tests/TreeTest.js
Normal file
@ -0,0 +1,7 @@
|
||||
const selectionMode = document.querySelector('#selection-mode');
|
||||
const tree = document.querySelector('.tree-selectable');
|
||||
|
||||
selectionMode.addEventListener('sl-change', () => {
|
||||
tree.querySelectorAll('sl-tree-item').forEach(item => (item.selected = false));
|
||||
tree.selection = selectionMode.value;
|
||||
});
|
26
api/js/etemplate/Et2TreeWidget/Tests/TreeTestSite.html
Normal file
26
api/js/etemplate/Et2TreeWidget/Tests/TreeTestSite.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TestSite</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<sl-tree class="tree-selectable">
|
||||
<sl-tree-item>
|
||||
Item 1
|
||||
<sl-tree-item>
|
||||
Item A
|
||||
<sl-tree-item>Item Z</sl-tree-item>
|
||||
<sl-tree-item>Item Y</sl-tree-item>
|
||||
<sl-tree-item>Item X</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
<script src="TreeTest.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -8,7 +8,8 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import {css, dedupeMixin, html, LitElement, SlotMixin} from '@lion/core';
|
||||
import {css, html, LitElement} from 'lit';
|
||||
import {dedupeMixin, SlotMixin} from '@lion/core';
|
||||
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {colorsDefStyles} from "../Styles/colorsDefStyles";
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
import {Et2InvokerMixin} from "./Et2InvokerMixin";
|
||||
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
|
||||
import {colorsDefStyles} from "../Styles/colorsDefStyles";
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,7 @@ import {Et2InvokerMixin} from "./Et2InvokerMixin";
|
||||
import {IsEmail} from "../Validators/IsEmail";
|
||||
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
|
||||
import {colorsDefStyles} from "../Styles/colorsDefStyles";
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
import {egw} from "../../jsapi/egw_global";
|
||||
|
||||
/**
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user