2022-08-22 16:43:41 +02:00
|
|
|
/**
|
|
|
|
* EGroupware eTemplate2 - Common button code
|
|
|
|
*
|
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
|
|
* @package etemplate
|
|
|
|
* @subpackage api
|
|
|
|
* @link https://www.egroupware.org
|
|
|
|
* @author Nathan Gray
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2023-09-13 19:55:33 +02:00
|
|
|
import {css, LitElement, PropertyValues} from "lit";
|
2022-08-22 16:43:41 +02:00
|
|
|
import '../Et2Image/Et2Image';
|
|
|
|
import shoelace from "../Styles/shoelace";
|
2023-11-08 22:37:30 +01:00
|
|
|
import {egw_registerGlobalShortcut} from "../../egw_action/egw_keymanager";
|
2022-08-22 16:43:41 +02:00
|
|
|
|
|
|
|
type Constructor<T = LitElement> = new (...args : any[]) => T;
|
|
|
|
export const ButtonMixin = <T extends Constructor>(superclass : T) => class extends superclass
|
|
|
|
{
|
|
|
|
protected clicked : boolean = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* images to be used as background-image, if none is explicitly applied and id matches given regular expression
|
|
|
|
*/
|
|
|
|
static readonly default_background_images : object = {
|
|
|
|
save: /save(&|\]|$)/,
|
|
|
|
apply: /apply(&|\]|$)/,
|
|
|
|
cancel: /cancel(&|\]|$)/,
|
|
|
|
delete: /delete(&|\]|$)/,
|
|
|
|
discard: /discard(&|\]|$)/,
|
2024-04-26 12:04:37 +02:00
|
|
|
edit: /edit(&|\[|\]|$)/,
|
2022-08-22 16:43:41 +02:00
|
|
|
next: /(next|continue)(&|\]|$)/,
|
|
|
|
finish: /finish(&|\]|$)/,
|
|
|
|
back: /(back|previous)(&|\]|$)/,
|
|
|
|
copy: /copy(&|\]|$)/,
|
|
|
|
more: /more(&|\]|$)/,
|
|
|
|
check: /(yes|check)(&|\]|$)/,
|
|
|
|
cancelled: /no(&|\]|$)/,
|
|
|
|
ok: /ok(&|\]|$)/,
|
|
|
|
close: /close(&|\]|$)/,
|
2023-11-24 06:26:49 +01:00
|
|
|
link: /link(&|\]|_|$)/,
|
2022-08-22 16:43:41 +02:00
|
|
|
add: /(add(&|\]|$)|create)/ // customfields use create*
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Classnames added automatically to buttons to set certain hover background colors
|
|
|
|
*/
|
|
|
|
static readonly default_classes : object = {
|
|
|
|
et2_button_cancel: /cancel(&|\]|$)/, // yellow
|
|
|
|
et2_button_question: /(yes|no)(&|\]|$)/, // yellow
|
|
|
|
et2_button_delete: /delete(&|\]|$)/ // red
|
|
|
|
};
|
|
|
|
|
2023-11-08 22:37:30 +01:00
|
|
|
static readonly default_keys : object = {
|
|
|
|
//egw_shortcutIdx : id regex
|
|
|
|
_83_C: /save(&|\]|$)/, // CTRL+S
|
|
|
|
_27_: /cancel(&|\]|$)/, // Esc
|
|
|
|
};
|
|
|
|
|
2022-08-22 16:43:41 +02:00
|
|
|
static get styles()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
...shoelace,
|
|
|
|
...(super.styles || []),
|
|
|
|
css`
|
|
|
|
:host {
|
|
|
|
padding: 0;
|
|
|
|
/* These should probably come from somewhere else */
|
|
|
|
max-width: 125px;
|
|
|
|
min-width: fit-content;
|
2022-12-01 21:03:39 +01:00
|
|
|
display: block;
|
2022-08-22 16:43:41 +02:00
|
|
|
}
|
2022-09-22 23:20:21 +02:00
|
|
|
/* Override general disabled=hide from Et2Widget */
|
|
|
|
:host([disabled]) {
|
2022-12-01 21:03:39 +01:00
|
|
|
display: block;
|
2022-09-22 23:20:21 +02:00
|
|
|
}
|
2022-08-22 16:43:41 +02:00
|
|
|
:host([hideonreadonly][disabled]) {
|
2022-10-21 13:29:08 +02:00
|
|
|
display:none !important;
|
2022-08-22 16:43:41 +02:00
|
|
|
}
|
2022-12-22 22:14:10 +01:00
|
|
|
|
|
|
|
/* Leave label there for accessability, but position it so it can't be seen */
|
|
|
|
:host(.imageOnly) .button__label {
|
|
|
|
position: absolute;
|
|
|
|
left: -999px
|
|
|
|
}
|
2022-09-22 23:20:21 +02:00
|
|
|
|
2022-08-22 16:43:41 +02:00
|
|
|
/* Set size for icon */
|
|
|
|
::slotted(img.imageOnly) {
|
|
|
|
padding-right: 0px !important;
|
|
|
|
width: 16px !important;
|
|
|
|
}
|
|
|
|
::slotted(et2-image) {
|
2022-11-23 22:35:36 +01:00
|
|
|
width: 20px;
|
2022-08-22 16:43:41 +02:00
|
|
|
max-width: 20px;
|
|
|
|
display: flex;
|
|
|
|
}
|
|
|
|
::slotted([slot="icon"][src='']) {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
.imageOnly {
|
|
|
|
width:18px;
|
|
|
|
height: 18px;
|
|
|
|
}
|
|
|
|
/* Make hover border match other widgets (select) */
|
|
|
|
.button--standard.button--default:hover:not(.button--disabled) {
|
2022-09-05 10:10:11 +02:00
|
|
|
background-color: var(--sl-color-gray-150);
|
2022-08-22 16:43:41 +02:00
|
|
|
border-color: var(--sl-input-border-color-hover);
|
|
|
|
color: var(--sl-input-color-hover);
|
|
|
|
}
|
|
|
|
.button {
|
|
|
|
justify-content: left;
|
|
|
|
}
|
|
|
|
.button--has-label.button--medium .button__label {
|
|
|
|
padding: 0 var(--sl-spacing-medium);
|
|
|
|
}
|
|
|
|
.button__label {
|
2022-09-08 12:47:11 +02:00
|
|
|
text-overflow: ellipsis;
|
|
|
|
overflow-x: hidden;
|
2022-08-22 16:43:41 +02:00
|
|
|
}
|
|
|
|
.button__prefix {
|
|
|
|
padding-left: 1px;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Only image, no label */
|
|
|
|
.button--has-prefix:not(.button--has-label) {
|
|
|
|
justify-content: center;
|
|
|
|
width: var(--sl-input-height-medium);
|
|
|
|
padding-inline-start: 0;
|
|
|
|
}
|
|
|
|
|
2022-12-02 19:23:09 +01:00
|
|
|
/* Override primary styling - we use variant=primary on first dialog button */
|
|
|
|
.button--standard.button--primary {
|
2024-07-13 06:56:54 +02:00
|
|
|
background-color: hsl(240deg 5% 96%);
|
2022-12-02 19:23:09 +01:00
|
|
|
border-color: var(--sl-color-gray-400);
|
|
|
|
color: var(--sl-input-color-hover);
|
|
|
|
}
|
|
|
|
.button--standard.button--primary:hover:not(.button--disabled),
|
|
|
|
.button--standard.button--primary.button--checked:not(.button--disabled) {
|
2022-12-05 18:54:05 +01:00
|
|
|
background-color: var(--sl-color-gray-150);
|
2022-12-02 19:23:09 +01:00
|
|
|
border-color: var(--sl-color-gray-600);
|
|
|
|
color: initial;
|
|
|
|
}
|
|
|
|
.button--standard.button--primary:active:not(.button--disabled) {
|
|
|
|
border-color: var(--sl-color-gray-700);
|
|
|
|
background-color: var(--sl-color-gray-300);
|
|
|
|
color: initial;
|
|
|
|
}
|
2022-08-22 16:43:41 +02:00
|
|
|
`,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
2023-01-04 23:03:43 +01:00
|
|
|
image: {type: String, noAccessor: true},
|
2022-08-22 16:43:41 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* If button is set to readonly, do we want to hide it completely (old behaviour) or show it as disabled
|
|
|
|
* (default)
|
|
|
|
* Something's not quite right here, as the attribute shows up as "hideonreadonly" instead of "hide" but
|
|
|
|
* it does not show up without the "attribute", and attribute:"hideonreadonly" does not show as an attribute
|
|
|
|
*/
|
|
|
|
hideOnReadonly: {type: Boolean, reflect: true, attribute: "hide"},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Button should submit the etemplate
|
|
|
|
* Return false from the click handler to cancel the submit, or set noSubmit to true to skip submitting.
|
|
|
|
*/
|
|
|
|
noSubmit: {type: Boolean, reflect: false},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When submitting, skip the validation step. Allows to submit etemplates directly to the server.
|
|
|
|
*/
|
|
|
|
noValidation: {type: Boolean}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(...args : any[])
|
|
|
|
{
|
|
|
|
super(...args);
|
|
|
|
|
|
|
|
// Property default values
|
|
|
|
this.__image = '';
|
|
|
|
this.noSubmit = false;
|
|
|
|
this.hideOnReadonly = false;
|
|
|
|
this.noValidation = false;
|
|
|
|
|
|
|
|
// Do not add icon here, no children can be added in constructor
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
set image(new_image : string)
|
|
|
|
{
|
|
|
|
let oldValue = this.__image;
|
|
|
|
if(new_image.indexOf("http") >= 0 || new_image.indexOf(this.egw().webserverUrl) >= 0)
|
|
|
|
{
|
|
|
|
this.__image = new_image
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.__image = this.egw().image(new_image);
|
|
|
|
}
|
|
|
|
this.requestUpdate("image", oldValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
get image()
|
|
|
|
{
|
|
|
|
return this.__image;
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleClick(event : MouseEvent) : boolean
|
|
|
|
{
|
|
|
|
// ignore click on readonly button
|
2022-10-06 13:55:24 +02:00
|
|
|
if(this.disabled || this.readonly || event.defaultPrevented)
|
2022-08-22 16:43:41 +02:00
|
|
|
{
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.clicked = true;
|
|
|
|
|
|
|
|
// Cancel buttons don't trigger the close confirmation prompt
|
|
|
|
if(this.classList.contains("et2_button_cancel"))
|
|
|
|
{
|
|
|
|
this.getInstanceManager()?.skip_close_prompt();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!super._handleClick(event))
|
|
|
|
{
|
|
|
|
this.clicked = false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Submit the form
|
|
|
|
if(!this.noSubmit)
|
|
|
|
{
|
|
|
|
return this.getInstanceManager().submit(this, undefined, this.noValidation);
|
|
|
|
}
|
|
|
|
this.clicked = false;
|
|
|
|
this.getInstanceManager()?.skip_close_prompt(false);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle changes that have to happen based on changes to properties
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
requestUpdate(name : PropertyKey, oldValue)
|
|
|
|
{
|
|
|
|
super.requestUpdate(name, oldValue);
|
|
|
|
|
|
|
|
// "disabled" is the attribute from the spec
|
|
|
|
if(name == 'readonly')
|
|
|
|
{
|
|
|
|
if(this.readonly)
|
|
|
|
{
|
|
|
|
this.setAttribute('disabled', "");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.removeAttribute("disabled");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Default image & class are determined based on ID
|
|
|
|
if(name == "id" && this._widget_id)
|
|
|
|
{
|
|
|
|
// Check against current value to avoid triggering another update
|
|
|
|
if(!this.image)
|
|
|
|
{
|
|
|
|
let image = this._get_default_image(this._widget_id);
|
|
|
|
if(image && image != this.__image)
|
|
|
|
{
|
|
|
|
this.image = image;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let default_class = this._get_default_class(this._widget_id);
|
|
|
|
if(default_class && !this.classList.contains(default_class))
|
|
|
|
{
|
|
|
|
this.classList.add(default_class);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updated(changedProperties : PropertyValues)
|
|
|
|
{
|
|
|
|
super.updated(changedProperties);
|
|
|
|
|
|
|
|
if(changedProperties.has("image"))
|
|
|
|
{
|
|
|
|
if(this.image && !this._iconNode)
|
|
|
|
{
|
|
|
|
const image = document.createElement("et2-image");
|
|
|
|
image.slot = "prefix";
|
|
|
|
this.prepend(image);
|
|
|
|
image.src = this.__image;
|
|
|
|
}
|
|
|
|
else if(this._iconNode)
|
|
|
|
{
|
|
|
|
this._iconNode.src = this.__image;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a default image for the button based on ID
|
|
|
|
*
|
|
|
|
* @param {string} check_id
|
|
|
|
*/
|
|
|
|
_get_default_image(check_id : string) : string
|
|
|
|
{
|
|
|
|
if(!check_id)
|
|
|
|
{
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!this.image)
|
|
|
|
{
|
2023-01-17 00:15:22 +01:00
|
|
|
// @ts-ignore
|
|
|
|
for(const image in this.constructor.default_background_images)
|
2022-08-22 16:43:41 +02:00
|
|
|
{
|
2023-01-17 00:15:22 +01:00
|
|
|
// @ts-ignore
|
|
|
|
if(check_id.match(this.constructor.default_background_images[image]))
|
2022-08-22 16:43:41 +02:00
|
|
|
{
|
|
|
|
return image;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2023-11-08 22:37:30 +01:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-22 16:43:41 +02:00
|
|
|
/**
|
|
|
|
* Get a default class for the button based on ID
|
|
|
|
*
|
|
|
|
* @param check_id
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
_get_default_class(check_id)
|
|
|
|
{
|
|
|
|
if(!check_id)
|
|
|
|
{
|
|
|
|
return "";
|
|
|
|
}
|
2023-01-05 17:00:02 +01:00
|
|
|
for(var name in ButtonMixin.default_classes)
|
2022-08-22 16:43:41 +02:00
|
|
|
{
|
2023-01-05 17:00:02 +01:00
|
|
|
if(check_id.match(ButtonMixin.default_classes[name]))
|
2022-08-22 16:43:41 +02:00
|
|
|
{
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
get _iconNode() : HTMLImageElement
|
|
|
|
{
|
|
|
|
return <HTMLImageElement>(Array.from(this.children)).find(
|
|
|
|
el => (<HTMLElement>el).slot === "prefix",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
get _labelNode() : HTMLElement
|
|
|
|
{
|
|
|
|
return <HTMLImageElement>(Array.from(this.childNodes)).find(
|
|
|
|
el => (<HTMLElement>el).nodeType === 3,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implementation of the et2_IInput interface
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Always return false as a button is never dirty
|
|
|
|
*/
|
|
|
|
isDirty()
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
resetDirty()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
getValue()
|
|
|
|
{
|
|
|
|
if(this.clicked)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If "null" is returned, the result is not added to the submitted
|
|
|
|
// array.
|
|
|
|
return null;
|
|
|
|
}
|
2024-04-26 12:04:37 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Reimplemented to pass aria-attributes to button
|
|
|
|
*/
|
|
|
|
getInputNode()
|
|
|
|
{
|
|
|
|
return this.shadowRoot.querySelector('button');
|
|
|
|
}
|
2022-08-22 20:24:40 +02:00
|
|
|
}
|