Et2TreeDropdown WIP

Initial commit.  Sort of works.
This commit is contained in:
nathan 2024-02-12 17:19:24 -07:00
parent b9f0ef7c41
commit 94a32d2800
4 changed files with 456 additions and 0 deletions

View File

@ -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;
}
`;

View File

@ -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 = <string[]>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`
<et2-image slot="prefix" part="icon" style="width: var(--icon-width)"
src="${option.icon}"></et2-image>`
}
inputTemplate()
{
return html`
<input id="search" type="text" part="input"
class="tree-dropdown__search"
exportparts="base:search__base"
autocomplete="off"
?disabled=${this.disabled}
?readonly=${this.readonly}
placeholder="${this.hasFocus || this.value.length > 0 || this.disabled || this.readonly ? "" : this.placeholder}"
tabindex="0"
@keydown=${this.handleSearchKeyDown}
@blur=${this.handleSearchBlur}
@focus=${this.handleSearchFocus}
@paste=${this.handlePaste}
/>
`;
}
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`
<et2-tag
part="tag"
exportparts="
base:tag__base,
content:tag__content,
remove-button:tag__remove-button,
remove-button__base:tag__remove-button__base,
icon:icon
"
class=${"tree_tag " + option.class ?? ""}
tabindex="-1"
?pill=${this.pill}
size=${this.size || "medium"}
?removable=${!readonly}
?readonly=${readonly}
?editable=${isEditable}
.value=${option.id}
@sl-remove=${(e : SlRemoveEvent) => 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()}
</et2-tag>
`;
}
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`
<div
part="form-control"
class=${classMap({
'form-control': true,
'form-control--medium': true,
'form-control--has-label': hasLabel,
'form-control--has-help-text': hasHelpText
})}
>
<label
id="label"
part="form-control-label"
class="form-control__label"
aria-hidden=${hasLabel ? 'false' : 'true'}
>
<slot name="label">${this.label}</slot>
</label>
<div part="form-control-input" class="form-control-input">
<sl-popup
class=${classMap({
"tree-dropdown": true,
input: true,
'tree-dropdown--open': this.open,
'tree-dropdown--disabled': this.disabled,
'tree-dropdown--readonly': this.readonly,
'tree-dropdown--focused': this.hasFocus,
'tree-dropdown--placeholder-visible': isPlaceholderVisible,
})}
strategy="fixed"
flip
shift
sync="width"
auto-size="vertical"
auto-size-padding="10"
?active=${this.open}
placement=${this.placement || "bottom"}
stay-open-on-select
?disabled=${this.disabled}
>
<div
part="combobox"
class="tree-dropdown__combobox"
slot="anchor"
@keydown=${this.handleComboboxKeyDown}
>
<slot part="prefix" name="prefix" class="tree-dropdown__prefix"></slot>
${this.tagsTemplate()}
${this.inputTemplate()}
${this.searching ? html`
<sl-spinner class="tree-dropdown"></sl-spinner>` : nothing
}
<slot part="suffix" name="suffix" class="tree-dropdown__suffix"></slot>
<et2-button caret class="tree-dropdown__trigger"
@click=${this.handleTriggerClick}
.noSubmit=${true}
>
</et2-button>
</div>
<et2-tree
class="tree-dropdown__tree"
multiple=${this.multiple}
?readonly=${this.readonly}
?disabled=${this.disabled}
.value=${this.value}
._selectOptions=${this.select_options}
@change=${this.handleTreeChange}
>
</et2-tree>
</sl-popup>
</div>
`
}
}
customElements.define("et2-tree-dropdown", Et2TreeDropdown);

View File

@ -105,6 +105,7 @@ import "./Et2Vfs/Et2VfsUid";
import "./Et2Textbox/Et2Password"; import "./Et2Textbox/Et2Password";
import './Et2Textbox/Et2Searchbox'; import './Et2Textbox/Et2Searchbox';
import "./Et2Tree/Et2Tree"; import "./Et2Tree/Et2Tree";
import "./Et2Tree/Et2TreeDropdown";
import "./Et2Tree/Et2MultiselectTree" import "./Et2Tree/Et2MultiselectTree"

View File

@ -486,6 +486,7 @@ class Tree extends Etemplate\Widget
ID on the form (addressbook edit): ID on the form (addressbook edit):
if tree overwrites selectbox options, selectbox will still work if tree overwrites selectbox options, selectbox will still work
*/ */
'value' => $cat['id'],
'label' => $s, 'label' => $s,
'title' => $cat['description'] 'title' => $cat['description']
); );