diff --git a/api/js/etemplate/Et2Select/Et2Listbox.ts b/api/js/etemplate/Et2Select/Et2Listbox.ts new file mode 100644 index 0000000000..75ea7235e1 --- /dev/null +++ b/api/js/etemplate/Et2Select/Et2Listbox.ts @@ -0,0 +1,160 @@ +import {SlMenu, SlMenuItem} from "@shoelace-style/shoelace"; +import {Et2widgetWithSelectMixin} from "./Et2WidgetWithSelectMixin"; +import {RowLimitedMixin} from "../Layout/RowLimitedMixin"; +import shoelace from "../Styles/shoelace"; +import {css, html, TemplateResult} from "@lion/core"; +import {SelectOption} from "./FindSelectOptions"; + +/** + * A selectbox that shows more than one row at a time + * + * Set rows attribute to adjust how many rows are visible at once + * + * Use Et2Selectbox in most cases, it's better. + */ +export class Et2Listbox extends RowLimitedMixin(Et2widgetWithSelectMixin(SlMenu)) +{ + + static get styles() + { + return [ + // Parent (SlMenu) returns a single cssResult, not an array + shoelace, + super.styles, + css` + :host { + display: block; + flex: 1 0 auto; + --icon-width: 20px; + } + + ::slotted(img), img { + vertical-align: middle; + } + + .menu { + /* Get rid of padding before/after options */ + padding: 0px; + + /* No horizontal scrollbar, even if options are long */ + overflow-x: clip; + } + /* Ellipsis when too small */ + sl-menu-item.menu-item__label { + display: block; + text-overflow: ellipsis; + /* This is usually not used due to flex, but is the basis for ellipsis calculation */ + width: 10ex; + } + + :host([rows])::part(base) { + max-height: calc(var(--rows, 5) * 1.9rem); + overflow-y: auto; + } + ` + ]; + } + + static get properties() + { + return { + ...super.properties, + /** + * Toggle between single and multiple selection + */ + multiple: { + type: Boolean, + reflect: true, + } + } + } + + constructor(...args : any[]) + { + super(); + this.handleSelect = this.handleSelect.bind(this); + } + + connectedCallback() + { + super.connectedCallback(); + + this.addEventListener("sl-select", this.handleSelect); + + this.updateComplete.then(() => + { + this.addEventListener("sl-change", this._triggerChange); + }); + } + + /** + * Handle an item was selected + * + * Toggle the checkmark and fire the changed event + * + * @param {MouseEvent} event + */ + handleSelect(event : CustomEvent) + { + let item = event.detail?.item; + if(!item) + { + return; + } + + if(!this.multiple) + { + this.getAllItems().forEach((i) => i.checked = false); + item.checked = true; + } + else + { + item.checked = !item.checked; + } + + this.dispatchEvent(new Event("change")); + } + + get value() + { + let value = this.getAllItems() + .filter((item) => item.checked) + .map((item) => item.value); + return this.multiple ? value : value.pop(); + } + + set value(new_value : String[] | String) + { + const oldValue = this.value; + if(typeof new_value == "string") + { + new_value = [new_value] + } + this.getAllItems().forEach((item) => item.checked = false); + for(let i = 0; i < new_value.length; i++) + { + const value = new_value[i]; + (this.querySelector("[value='" + value + "']")).checked = true; + } + this.requestUpdate("value", oldValue); + } + + _optionTemplate(option : SelectOption) : TemplateResult + { + let icon = option.icon ? html` + ` : ""; + + // 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` + + ${icon} + ${this.noLang ? option.label : this.egw().lang(option.label)} + `; + } +} + +customElements.define("et2-listbox", Et2Listbox); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_placeholder.ts b/api/js/etemplate/et2_widget_placeholder.ts index 2419e33ce9..61df192f8b 100644 --- a/api/js/etemplate/et2_widget_placeholder.ts +++ b/api/js/etemplate/et2_widget_placeholder.ts @@ -173,6 +173,15 @@ export class et2_placeholder_select extends et2_inputWidget this._do_insert_callback(submit_value); return true; } + else if(submit_button_id == 'cancel') + { + return true; + } + else + { + // Keep dialog open + return false; + } }.bind(this); this.dialog = new Et2Dialog(this.egw()); @@ -448,12 +457,12 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select app.onchange = (node, widget) => { entry.set_value({app: widget.get_value()}); - placeholder_list.set_select_options(this._get_placeholders(app.get_value(), "addresses")); + placeholder_list.set_select_options(this._get_placeholders(app.value, "addresses")); } placeholder_list.onchange = this._on_placeholder_select.bind(this); entry.onchange = this._on_placeholder_select.bind(this); - app.set_value(app.get_value()); + app.set_value(app.value); this._on_placeholder_select(); } @@ -463,16 +472,22 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select */ _on_placeholder_select() { + let app = this.dialog.template.widgetContainer.getDOMWidgetById("app"); let entry = this.dialog.template.widgetContainer.getDOMWidgetById("entry"); let placeholder_list = this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list"); let preview_content = this.dialog.template.widgetContainer.getDOMWidgetById("preview_content"); + let placeholder = ""; + if(app && app.value) + { + placeholder = Object.keys(et2_placeholder_snippet_select.placeholders[app.value]["addresses"])[placeholder_list.value]; + } - if(placeholder_list.value && entry.get_value()) + if(placeholder && entry.get_value()) { // Show the selected placeholder replaced with value from the selected entry this.egw().json( 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders', - [placeholder_list.value, {app: "addressbook", id: entry.get_value()}], + [placeholder, {app: "addressbook", id: entry.get_value()}], function(_content) { if(!_content) @@ -525,11 +540,11 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select _get_placeholders(appname : string, group : string) { let options = []; - Object.keys(et2_placeholder_snippet_select.placeholders[appname][group]).map((key) => + Object.keys(et2_placeholder_snippet_select.placeholders[appname][group]).map((key, index) => { options.push( { - value: key, + value: index, label: this.egw().lang(et2_placeholder_snippet_select.placeholders[appname][group][key]) }); }); diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index c32dcc4dc8..166c7d50ac 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -66,6 +66,7 @@ import './Et2Nextmatch/Headers/AccountFilterHeader'; import './Et2Nextmatch/Headers/CustomFilterHeader'; import './Et2Nextmatch/Headers/EntryHeader'; import './Et2Nextmatch/Headers/FilterHeader'; +import './Et2Select/Et2Listbox'; import './Et2Select/Et2Select'; import './Et2Select/Et2SelectAccount'; import './Et2Select/Et2SelectCategory'; diff --git a/api/templates/default/insert_merge_placeholder.xet b/api/templates/default/insert_merge_placeholder.xet index f5c21e4f7d..30155344af 100644 --- a/api/templates/default/insert_merge_placeholder.xet +++ b/api/templates/default/insert_merge_placeholder.xet @@ -10,12 +10,12 @@ + + align="right" image="export"> @@ -23,6 +23,7 @@ diff --git a/api/templates/default/placeholder_snippet.xet b/api/templates/default/placeholder_snippet.xet index 20e433e770..12f52411b0 100644 --- a/api/templates/default/placeholder_snippet.xet +++ b/api/templates/default/placeholder_snippet.xet @@ -6,7 +6,7 @@ +