From f0dcc1685d5b6fcb7f416d3bcad3fc287fd6b6e5 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 22 Aug 2022 08:43:41 -0600 Subject: [PATCH] Move our button code into a mixin and extend sl-button to our current et2-button and sl-icon-button to a new et2-button-icon. --- api/js/etemplate/Et2Button/ButtonMixin.ts | 357 ++++++++++++++++++ api/js/etemplate/Et2Button/Et2Button.ts | 383 +------------------- api/js/etemplate/Et2Button/Et2ButtonIcon.ts | 23 ++ 3 files changed, 383 insertions(+), 380 deletions(-) create mode 100644 api/js/etemplate/Et2Button/ButtonMixin.ts create mode 100644 api/js/etemplate/Et2Button/Et2ButtonIcon.ts diff --git a/api/js/etemplate/Et2Button/ButtonMixin.ts b/api/js/etemplate/Et2Button/ButtonMixin.ts new file mode 100644 index 0000000000..af1c28194e --- /dev/null +++ b/api/js/etemplate/Et2Button/ButtonMixin.ts @@ -0,0 +1,357 @@ +/** + * 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 + */ + + +import {css, LitElement, PropertyValues} from "@lion/core"; +import '../Et2Image/Et2Image'; +import shoelace from "../Styles/shoelace"; +import {Et2Button} from "./Et2Button"; + +type Constructor = new (...args : any[]) => T; +export const ButtonMixin = (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(&|\]|$)/, + edit: /edit(&|\[\]|$)/, + next: /(next|continue)(&|\]|$)/, + finish: /finish(&|\]|$)/, + back: /(back|previous)(&|\]|$)/, + copy: /copy(&|\]|$)/, + more: /more(&|\]|$)/, + check: /(yes|check)(&|\]|$)/, + cancelled: /no(&|\]|$)/, + ok: /ok(&|\]|$)/, + close: /close(&|\]|$)/, + add: /(add(&|\]|$)|create)/ // customfields use create* + }; + + /** + * Classnames added automatically to buttons to set certain hover background colors + */ + static readonly default_classes : object = { + et2_button_cancel: /cancel(&|\]|$)/, // yellow + et2_button_question: /(yes|no)(&|\]|$)/, // yellow + et2_button_delete: /delete(&|\]|$)/ // red + }; + + static get styles() + { + return [ + ...shoelace, + ...(super.styles || []), + css` + :host { + padding: 0; + /* These should probably come from somewhere else */ + max-width: 125px; + min-width: fit-content; + } + :host([hideonreadonly][disabled]) { + display:none; + } + /* Set size for icon */ + ::slotted(img.imageOnly) { + padding-right: 0px !important; + width: 16px !important; + } + ::slotted(et2-image) { + 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) { + background-color: var(--sl-input-background-color-hover); + 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 { + } + .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; + } + + `, + ]; + } + + static get properties() + { + return { + ...super.properties, + label: {type: String}, + image: {type: String}, + + /** + * 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 + if(this.disabled || this.readonly) + { + 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) + { + for(const image in Et2Button.default_background_images) + { + if(check_id.match(Et2Button.default_background_images[image])) + { + return image; + } + } + } + return ""; + } + + /** + * Get a default class for the button based on ID + * + * @param check_id + * @returns {string} + */ + _get_default_class(check_id) + { + if(!check_id) + { + return ""; + } + for(var name in Et2Button.default_classes) + { + if(check_id.match(Et2Button.default_classes[name])) + { + return name; + } + } + return ""; + } + + get _iconNode() : HTMLImageElement + { + return (Array.from(this.children)).find( + el => (el).slot === "prefix", + ); + } + + get _labelNode() : HTMLElement + { + return (Array.from(this.childNodes)).find( + el => (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; + } +} diff --git a/api/js/etemplate/Et2Button/Et2Button.ts b/api/js/etemplate/Et2Button/Et2Button.ts index 1de0741b3f..dd592c5e82 100644 --- a/api/js/etemplate/Et2Button/Et2Button.ts +++ b/api/js/etemplate/Et2Button/Et2Button.ts @@ -9,392 +9,15 @@ */ -import {css, PropertyValues} from "@lion/core"; import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget"; import '../Et2Image/Et2Image'; import {SlButton} from "@shoelace-style/shoelace"; -import shoelace from "../Styles/shoelace"; +import {ButtonMixin} from "./ButtonMixin"; -export class Et2Button extends Et2InputWidget(SlButton) + +export class Et2Button extends ButtonMixin(Et2InputWidget(SlButton)) { - 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(&|\]|$)/, - edit: /edit(&|\[\]|$)/, - next: /(next|continue)(&|\]|$)/, - finish: /finish(&|\]|$)/, - back: /(back|previous)(&|\]|$)/, - copy: /copy(&|\]|$)/, - more: /more(&|\]|$)/, - check: /(yes|check)(&|\]|$)/, - cancelled: /no(&|\]|$)/, - ok: /ok(&|\]|$)/, - close: /close(&|\]|$)/, - add: /(add(&|\]|$)|create)/ // customfields use create* - }; - - /** - * Classnames added automatically to buttons to set certain hover background colors - */ - static readonly default_classes : object = { - et2_button_cancel: /cancel(&|\]|$)/, // yellow - et2_button_question: /(yes|no)(&|\]|$)/, // yellow - et2_button_delete: /delete(&|\]|$)/ // red - }; - - static get styles() - { - return [ - ...shoelace, - ...super.styles, - css` - :host { - padding: 0; - /* These should probably come from somewhere else */ - max-width: 125px; - min-width: fit-content; - } - :host([hideonreadonly][disabled]) { - display:none; - } - /* Set size for icon */ - ::slotted(img.imageOnly) { - padding-right: 0px !important; - width: 16px !important; - } - ::slotted(et2-image) { - 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) { - background-color: var(--sl-input-background-color-hover); - 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 { - } - .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; - } - - `, - ]; - } - - static get properties() - { - return { - ...super.properties, - // LionButton doesn't have a label property & Et2Widget avoids re-defining it - label: {type: String}, - image: {type: String}, - - /** - * 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 - - } - - protected firstUpdated(_changedProperties : PropertyValues) - { - super.firstUpdated(_changedProperties); - - if(!this.label && this.__image) - { - /* - Label / no label should get special classes set, but they're missing without this extra requestUpdate() - This is a work-around for button--has-prefix & button--has-label not being set, something to do - with how we're setting them. - */ - this.updateComplete.then(() => - { - this.requestUpdate(); - }); - } - } - - set label(new_label : string) - { - this.updateComplete.then(() => - { - if(!this._labelNode) - { - const textNode = document.createTextNode(new_label); - this.appendChild(textNode); - } - else - { - this._labelNode.textContent = new_label; - } - }); - } - - get label() - { - return this._labelNode?.textContent?.trim(); - } - - set image(new_image : string) - { - let oldValue = this.__image; - if(new_image.indexOf("http") >= 0 || new_image.indexOf(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 - if(this.disabled || this.readonly) - { - 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) - { - for(const image in Et2Button.default_background_images) - { - if(check_id.match(Et2Button.default_background_images[image])) - { - return image; - } - } - } - return ""; - } - - /** - * Get a default class for the button based on ID - * - * @param check_id - * @returns {string} - */ - _get_default_class(check_id) - { - if(!check_id) - { - return ""; - } - for(var name in Et2Button.default_classes) - { - if(check_id.match(Et2Button.default_classes[name])) - { - return name; - } - } - return ""; - } - - get _iconNode() : HTMLImageElement - { - return (Array.from(this.children)).find( - el => (el).slot === "prefix", - ); - } - - get _labelNode() : HTMLElement - { - return (Array.from(this.childNodes)).find( - el => (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; - } } -// @ts-ignore TypeScript is not recognizing that Et2Button is a LitElement customElements.define("et2-button", Et2Button); \ No newline at end of file diff --git a/api/js/etemplate/Et2Button/Et2ButtonIcon.ts b/api/js/etemplate/Et2Button/Et2ButtonIcon.ts new file mode 100644 index 0000000000..71b970a4d8 --- /dev/null +++ b/api/js/etemplate/Et2Button/Et2ButtonIcon.ts @@ -0,0 +1,23 @@ +/** + * EGroupware eTemplate2 - Button that's just an image + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link https://www.egroupware.org + * @author Nathan Gray + */ + + +import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget"; +import '../Et2Image/Et2Image'; +import {SlIconButton} from "@shoelace-style/shoelace"; +import {ButtonMixin} from "./ButtonMixin"; + + +export class Et2ButtonIcon extends ButtonMixin(Et2InputWidget(SlIconButton)) +{ + +} + +customElements.define("et2-button-icon", Et2ButtonIcon); \ No newline at end of file