diff --git a/api/js/etemplate/Et2Tree/Et2TreeDropdown.styles.ts b/api/js/etemplate/Et2Tree/Et2TreeDropdown.styles.ts new file mode 100644 index 0000000000..cac700a610 --- /dev/null +++ b/api/js/etemplate/Et2Tree/Et2TreeDropdown.styles.ts @@ -0,0 +1,159 @@ +import {css} from 'lit'; + +export default css` + .form-control .form-control__label { + display: none; + } + + .form-control .form-control__help-text { + display: none; + } + + /* Label */ + + .form-control--has-label .form-control__label { + display: inline-block; + color: var(--sl-input-label-color); + margin-bottom: var(--sl-spacing-3x-small); + } + + .form-control--has-label.form-control--small .form-control__label { + font-size: var(--sl-input-label-font-size-small); + } + + .form-control--has-label.form-control--medium .form-control__label { + font-size: var(--sl-input-label-font-size-medium); + } + + .form-control--has-label.form-control--large .form-control__label { + font-size: var(--sl-input-label-font-size-large); + } + + /* Help text */ + + .form-control--has-help-text .form-control__help-text { + display: block; + color: var(--sl-input-help-text-color); + margin-top: var(--sl-spacing-3x-small); + } + + .tree-dropdown__combobox { + flex: 1; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 0.1rem 0.5rem; + + background-color: var(--sl-input-background-color); + border: solid var(--sl-input-border-width) var(--sl-input-border-color); + + border-radius: var(--sl-input-border-radius-medium); + font-size: var(--sl-input-font-size-medium); + min-height: var(--sl-input-height-medium); + max-height: calc(var(--height, 5) * var(--sl-input-height-medium)); + overflow-y: auto; + padding-block: 0; + padding-inline: var(--sl-input-spacing-medium); + padding-top: 0.1rem; + padding-bottom: 0.1rem; + + transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow, + var(--sl-transition-fast) background-color; + } + + .tree-dropdown--disabled { + background-color: var(--sl-input-background-color-disabled); + border-color: var(--sl-input-border-color-disabled); + color: var(--sl-input-color-disabled); + opacity: 0.5; + cursor: not-allowed; + outline: none; + } + + :not(.tree-dropdown--disabled).tree-dropdown--open, + :not(.tree-dropdown--disabled).tree-dropdown--focused { + background-color: var(--sl-input-background-color-focus); + border-color: var(--sl-input-border-color-focus); + box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-input-focus-ring-color); + } + + /* Trigger */ + + .tree-dropdown__trigger { + max-width: initial; + min-width: initial; + flex: 0 0 1em; + order: 21; + margin-left: auto; + } + + .tree-dropdown__trigger::part(base) { + border: none; + display: flex; + align-items: center; + } + + /* End trigger */ + + .tree-dropdown__prefix { + order: 1; + } + + /* Search box */ + + :host([readonly]) .tree-dropdown__search { + display: none; + } + + .tree-dropdown__search { + flex: 1 1 auto; + order: 10; + min-width: 7em; + border: none; + outline: none; + + font-size: var(--sl-input-font-size-medium); + padding-block: 0; + padding-inline: var(--sl-input-spacing-medium); + } + + .form-control--medium .tree-dropdown__search { + /* Input same size as tags */ + height: calc(var(--sl-input-height-medium) * 0.8); + } + + .tree-dropdown--disabled .tree-dropdown__search { + cursor: not-allowed; + } + + .tree-dropdown--readonly .tree-dropdown__search { + cursor: default; + } + + .tree-dropdown__suffix { + order: 20; + } + + /* Tree */ + + sl-popup::part(popup) { + font-size: var(--sl-font-size-medium); + font-weight: var(--sl-font-weight-normal); + box-shadow: var(--sl-shadow-large); + background: var(--sl-panel-background-color); + border: solid var(--sl-panel-border-width) var(--sl-panel-border-color); + border-radius: var(--sl-border-radius-medium); + padding-block: var(--sl-spacing-x-small); + padding-inline: 0; + overflow: auto; + overscroll-behavior: none; + z-index: var(--sl-z-index-dropdown); + + /* Make sure it adheres to the popup's auto size */ + max-width: var(--auto-size-available-width); + + /* This doesn't work for some reason, it's overwritten somewhere */ + --size: 1.8em; + } +`; \ No newline at end of file diff --git a/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts b/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts new file mode 100644 index 0000000000..3c2fcdbe30 --- /dev/null +++ b/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts @@ -0,0 +1,295 @@ +import {html, LitElement, nothing} from "lit"; +import {Et2Tree, TreeItemData} from "./Et2Tree"; +import {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin"; +import {property} from "lit/decorators/property.js"; +import {classMap} from "lit/directives/class-map.js"; +import {HasSlotController} from "../Et2Widget/slot"; +import {keyed} from "lit/directives/keyed.js"; +import {map} from "lit/directives/map.js"; +import {SlDropdown, SlRemoveEvent} from "@shoelace-style/shoelace"; +import shoelace from "../Styles/shoelace"; +import styles from "./Et2TreeDropdown.styles"; +import {literal, StaticValue} from "lit/static-html.js"; + +/** + * @summary A tree that is hidden in a dropdown + * + * @dependency sl-dropdown + * @dependency et2-tree + * @dependency et2-tag + * + * @slot label - The input's label. Alternatively, you can use the `label` attribute. + * @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute. + * + * @event change - Emitted when the control's value changes. + * @event sl-show - Emitted when the suggestion menu opens. + * @event sl-after-show - Emitted after the suggestion menu opens and all animations are complete. + * @event sl-hide - Emitted when the suggestion menu closes. + * @event sl-after-hide - Emitted after the suggestion menu closes and all animations are complete. + * + * @csspart form-control - The form control that wraps the label, input, and help text. + */ + +export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement) +{ + + static get styles() + { + return [ + shoelace, + ...super.styles, + styles + ]; + } + + /** Placeholder text to show as a hint when the select is empty. */ + @property() placeholder = ""; + + @property({type: Boolean, reflect: true}) multiple = false; + + /** The component's help text. If you need to display HTML, use the `help-text` slot instead. */ + @property({attribute: 'help-text'}) helpText = ""; + + /** + * Indicates whether the dropdown is open. You can toggle this attribute to show and hide the tree, or you can + * use the `show()` and `hide()` methods and this attribute will reflect the open state. + */ + @property({type: Boolean, reflect: true}) open = false; + + + private get _popup() : SlDropdown { return this.shadowRoot.querySelector("sl-popup")} + + private get _tree() : Et2Tree { return this.shadowRoot.querySelector("et2-tree")} + + protected readonly hasSlotController = new HasSlotController(this, "help-text", "label"); + private __value : string[]; + + constructor() + { + super(); + this.multiple = false; + this.__value = []; + } + + /** Selected tree leaves */ + @property() + set value(new_value : string | string[]) + { + if(typeof new_value === "string") + { + new_value = new_value.split(",") + } + const oldValue = this.__value; + this.__value = new_value; + this.requestUpdate("value", oldValue); + } + + get value() : string | string[] + { + return this.multiple ? this.__value : ( + this.__value?.length ? this.__value[0] : "" + ); + } + + handleTagRemove(event : SlRemoveEvent, value : string) + { + // Find the tag value and remove it from current value + const index = this.value.indexOf(value); + this.value.splice(index, 1); + this.requestUpdate("value"); + this.dispatchEvent(new Event("change", {bubbles: true})); + } + + handleTreeChange(event) + { + const oldValue = this.value; + this.value = this._tree.value; + this.requestUpdate("value", oldValue); + } + + handleTriggerClick() + { + if(this.open) + { + this._popup.active = false; + } + else + { + this._popup.active = true; + } + this.open = this._popup.active; + } + + /** + * Tag used for rendering tags when multiple=true + * Used for creating, finding & filtering options. + * @see createTagNode() + * @returns {string} + */ + public get tagTag() : StaticValue + { + return literal`et2-tag`; + } + + /** + * Get the icon for the select option + * + * @param option + * @protected + */ + protected iconTemplate(option) + { + if(!option.icon) + { + return html``; + } + + return html` + ` + } + + inputTemplate() + { + return html` + + `; + } + + tagsTemplate() + { + const value = this.getValueAsArray(); + return html`${keyed(this._valueUID, map(value, (value, index) => this.tagTemplate(this.optionSearch(value, this.select_options))))}`; + } + + tagTemplate(option : TreeItemData) + { + const readonly = (this.readonly || option && typeof (option.disabled) != "undefined" && option.disabled); + const isEditable = false && !readonly; + const image = this.iconTemplate(option.option ?? option); + return html` + this.handleTagRemove(e, option.id)} + @change=${this.handleTagEdit} + @dblclick=${this._handleDoubleClick} + @click=${typeof this.onTagClick == "function" ? (e) => this.onTagClick(e, e.target) : nothing} + > + ${image ?? nothing} + ${option.text.trim()} + + `; + } + + public render() + { + const hasLabelSlot = this.hasSlotController.test('label'); + const hasHelpTextSlot = this.hasSlotController.test('help-text'); + const hasLabel = this.label ? true : !!hasLabelSlot; + const hasHelpText = this.helpText ? true : !!hasHelpTextSlot; + const isPlaceholderVisible = this.placeholder && this.value.length === 0 && !this.disabled && !this.readonly; + + return html` +
+ +
+ +
+ + ${this.tagsTemplate()} + ${this.inputTemplate()} + ${this.searching ? html` + ` : nothing + } + + + +
+ + +
+
+ ` + } +} + +customElements.define("et2-tree-dropdown", Et2TreeDropdown); \ No newline at end of file diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 95fa6e8e3b..97c59854fa 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -105,6 +105,7 @@ import "./Et2Vfs/Et2VfsUid"; import "./Et2Textbox/Et2Password"; import './Et2Textbox/Et2Searchbox'; import "./Et2Tree/Et2Tree"; +import "./Et2Tree/Et2TreeDropdown"; import "./Et2Tree/Et2MultiselectTree" diff --git a/api/src/Etemplate/Widget/Tree.php b/api/src/Etemplate/Widget/Tree.php index 608b5bef70..ef279caef0 100644 --- a/api/src/Etemplate/Widget/Tree.php +++ b/api/src/Etemplate/Widget/Tree.php @@ -486,6 +486,7 @@ class Tree extends Etemplate\Widget ID on the form (addressbook edit): if tree overwrites selectbox options, selectbox will still work */ + 'value' => $cat['id'], 'label' => $s, 'title' => $cat['description'] );