mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-15 11:11:49 +01:00
217 lines
5.2 KiB
TypeScript
217 lines
5.2 KiB
TypeScript
/**
|
|
* EGroupware eTemplate2 - InvokerMixing
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package api
|
|
* @link https://www.egroupware.org
|
|
* @author Ralf Becker
|
|
*/
|
|
|
|
/* eslint-disable import/no-extraneous-dependencies */
|
|
import {css, html, LitElement, render} from 'lit';
|
|
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
|
import {colorsDefStyles} from "../Styles/colorsDefStyles";
|
|
import {dedupeMixin} from "@open-wc/dedupe-mixin";
|
|
|
|
/**
|
|
* Invoker mixing adds an invoker button to a widget to trigger some action, e.g.:
|
|
* - searchbox to delete input
|
|
* - url to open url
|
|
* - url-email to open mail compose
|
|
*
|
|
* Inspired by Lion date-picker.
|
|
*/
|
|
|
|
type Constructor<T = Et2InputWidgetInterface> = new (...args : any[]) => T;
|
|
export const Et2InvokerMixin = dedupeMixin(<T extends Constructor<LitElement>>(superclass : T) =>
|
|
{
|
|
class Et2Invoker extends Et2InputWidget(superclass)
|
|
{
|
|
/** @type {any} */
|
|
static get properties()
|
|
{
|
|
return {
|
|
/**
|
|
* Textual label or image specifier for egw.image()
|
|
*/
|
|
_invokerLabel: {
|
|
type: String,
|
|
},
|
|
_invokerTitle: {
|
|
type: String,
|
|
},
|
|
_invokerAction: {
|
|
type: Function,
|
|
}
|
|
};
|
|
}
|
|
|
|
static get styles()
|
|
{
|
|
return [
|
|
...super.styles,
|
|
colorsDefStyles,
|
|
css`
|
|
::slotted(input), input, ::slotted(select) {
|
|
background-color: transparent;
|
|
border: none !important;
|
|
}
|
|
.input-group {
|
|
border: 1px solid var(--input-border-color);
|
|
}
|
|
.input-group__suffix{
|
|
text-align: center;
|
|
}
|
|
.input-group__container {
|
|
align-items: center
|
|
}
|
|
::slotted([slot="suffix"]) {
|
|
width: 14px;
|
|
border: none !important;
|
|
background-color: transparent !important;
|
|
width: 1em;
|
|
height: 1em;
|
|
background-position: center right;
|
|
background-size: contain;
|
|
background-repeat: no-repeat;
|
|
}
|
|
::slotted(:disabled) {cursor: default !important;}
|
|
:host(:hover) ::slotted([slot="suffix"]) {
|
|
cursor: pointer;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @protected
|
|
*/
|
|
get _invokerNode()
|
|
{
|
|
return /** @type {HTMLElement} */ (this.querySelector(`#${this.__invokerId}`));
|
|
}
|
|
|
|
constructor(...args : any[])
|
|
{
|
|
super(...args);
|
|
/** @private */
|
|
this.__invokerId = this.__createUniqueIdForA11y();
|
|
// default for properties
|
|
this._invokerTitle = 'Click to open';
|
|
}
|
|
|
|
/** @private */
|
|
__createUniqueIdForA11y()
|
|
{
|
|
return `${this.localName}-${Math.random().toString(36).substr(2, 10)}`;
|
|
}
|
|
|
|
/**
|
|
* @param {PropertyKey} name
|
|
* @param {?} oldValue
|
|
*/
|
|
requestUpdate(name, oldValue)
|
|
{
|
|
super.requestUpdate(name, oldValue);
|
|
|
|
if (name === 'disabled' || name === 'showsFeedbackFor' || name === 'modelValue')
|
|
{
|
|
this._toggleInvokerDisabled();
|
|
}
|
|
|
|
if (name === '_invokerLabel' || name === '_invokerTitle')
|
|
{
|
|
this._toggleInvoker();
|
|
}
|
|
if (name === '_invokerAction')
|
|
{
|
|
if (oldValue) this._invokerNode?.removeEventListener('click', oldValue);
|
|
this._invokerNode?.addEventListener('click', this._invokerAction.bind(this), true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* (Un)Hide invoker, if no label or action defined
|
|
*
|
|
* @protected
|
|
* */
|
|
_toggleInvoker()
|
|
{
|
|
if (this._invokerNode)
|
|
{
|
|
this._invokerNode.style.display = !this._invokerLabel ? 'none' : 'inline-block';
|
|
const img = this._invokerLabel ? this.egw().image(this._invokerLabel) : null;
|
|
if (img)
|
|
{
|
|
this._invokerNode.style.backgroundImage = 'url('+img+')';
|
|
this._invokerNode.innerHTML = '';
|
|
}
|
|
else
|
|
{
|
|
this._invokerNode.style.backgroundImage = 'none';
|
|
this._invokerNode.innerHTML = this._invokerLabel || '';
|
|
}
|
|
this._invokerNode.title = this._invokerTitle || '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method to check if invoker can be activated: not disabled, empty or invalid
|
|
*
|
|
* @protected
|
|
* */
|
|
_toggleInvokerDisabled()
|
|
{
|
|
if (this._invokerNode)
|
|
{
|
|
const invokerNode = /** @type {HTMLElement & {disabled: boolean}} */ (this._invokerNode);
|
|
invokerNode.disabled = this.disabled || !this.value || (this.input && !this.input.reportValidity())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reimplemented to enable/disable invoker on content change
|
|
*
|
|
* @param _ev
|
|
* @returns
|
|
*/
|
|
_oldChange(_ev: Event): boolean
|
|
{
|
|
this._toggleInvokerDisabled();
|
|
|
|
return super._oldChange(_ev);
|
|
}
|
|
|
|
/** @param changedProperties */
|
|
firstUpdated(changedProperties)
|
|
{
|
|
super.firstUpdated(changedProperties);
|
|
render(this._invokerTemplate(), this);
|
|
this._toggleInvokerDisabled();
|
|
this._toggleInvoker();
|
|
}
|
|
|
|
/**
|
|
* Subclassers can replace this with their custom extension invoker,
|
|
* like `<my-button><calendar-icon></calendar-icon></my-button>`
|
|
*/
|
|
// eslint-disable-next-line class-methods-use-this
|
|
_invokerTemplate()
|
|
{
|
|
return html`
|
|
<button slot="suffix"
|
|
type="button"
|
|
class="et2-invoker__button"
|
|
@click="${this._invokerAction}"
|
|
id="${this.__invokerId}"
|
|
aria-label="${this._invokerTitle}"
|
|
title="${this._invokerTitle}"
|
|
>
|
|
${this._invokerLabel}
|
|
</button>
|
|
`;
|
|
}
|
|
}
|
|
|
|
return Et2Invoker as unknown as Constructor<Et2Invoker> & T;
|
|
}) |