From 1dbbac450cc25bab9df4b991597f469d412f3c99 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 26 Jun 2025 19:00:15 +0000 Subject: [PATCH] update dees-tags --- ts_web/elements/dees-input-tags.demo.ts | 248 +++++++++++++++ ts_web/elements/dees-input-tags.ts | 401 ++++++++++++++++++++++++ ts_web/elements/index.ts | 1 + ts_web/pages/input-showcase.ts | 23 ++ ts_web/pages/zindex-showcase.ts | 49 +++ 5 files changed, 722 insertions(+) create mode 100644 ts_web/elements/dees-input-tags.demo.ts diff --git a/ts_web/elements/dees-input-tags.demo.ts b/ts_web/elements/dees-input-tags.demo.ts new file mode 100644 index 0000000..3320f9d --- /dev/null +++ b/ts_web/elements/dees-input-tags.demo.ts @@ -0,0 +1,248 @@ +import { html, css } from '@design.estate/dees-element'; +import '@design.estate/dees-wcctools/demotools'; +import './dees-panel.js'; + +export const demoFunc = () => html` + + + +
+ + + + + + + + + +
+ + + +
+
+ + + + + + + + + + + + + +
+ + + +
+ + + + +
+
+ + + { + const preview = document.querySelector('#tags-preview'); + const tags = e.detail.value; + if (preview) { + if (tags.length === 0) { + preview.innerHTML = 'No tags added yet...'; + } else { + preview.innerHTML = tags.map((tag: string) => + `${tag}` + ).join(''); + } + } + }} + > + +
+ No tags added yet... +
+ +
+ JSON output will appear here... +
+ + +
+
+
+`; \ No newline at end of file diff --git a/ts_web/elements/dees-input-tags.ts b/ts_web/elements/dees-input-tags.ts index e69de29..4981df1 100644 --- a/ts_web/elements/dees-input-tags.ts +++ b/ts_web/elements/dees-input-tags.ts @@ -0,0 +1,401 @@ +import { + customElement, + html, + css, + cssManager, + property, + state, + type TemplateResult, +} from '@design.estate/dees-element'; +import { DeesInputBase } from './dees-input-base.js'; +import './dees-icon.js'; +import { demoFunc } from './dees-input-tags.demo.js'; + +declare global { + interface HTMLElementTagNameMap { + 'dees-input-tags': DeesInputTags; + } +} + +@customElement('dees-input-tags') +export class DeesInputTags extends DeesInputBase { + // STATIC + public static demo = demoFunc; + + // INSTANCE + @property({ type: Array }) + public value: string[] = []; + + @property({ type: String }) + public placeholder: string = 'Add tags...'; + + @property({ type: Number }) + public maxTags: number = 0; // 0 means unlimited + + @property({ type: Array }) + public suggestions: string[] = []; + + @state() + private inputValue: string = ''; + + @state() + private showSuggestions: boolean = false; + + @state() + private highlightedSuggestionIndex: number = -1; + + @property({ type: String }) + public validationText: string = ''; + + public static styles = [ + ...DeesInputBase.baseStyles, + cssManager.defaultStyles, + css` + :host { + display: block; + font-family: 'Geist Sans', sans-serif; + } + + .input-wrapper { + width: 100%; + } + + .tags-container { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 8px; + min-height: 40px; + background: ${cssManager.bdTheme('#fafafa', '#222222')}; + border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')}; + border-radius: 8px; + transition: all 0.2s ease; + cursor: text; + } + + .tags-container:focus-within { + border-color: ${cssManager.bdTheme('#0069f2', '#0084ff')}; + box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 105, 242, 0.1)', 'rgba(0, 132, 255, 0.2)')}; + } + + .tags-container.disabled { + background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')}; + cursor: not-allowed; + opacity: 0.6; + } + + .tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; + color: ${cssManager.bdTheme('#1976d2', '#90caf9')}; + border-radius: 16px; + font-size: 14px; + line-height: 1.2; + user-select: none; + animation: tagAppear 0.2s ease; + } + + @keyframes tagAppear { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } + } + + .tag-remove { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + margin-left: 2px; + } + + .tag-remove:hover { + background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; + } + + .tag-remove dees-icon { + width: 12px; + height: 12px; + } + + .tag-input { + flex: 1; + min-width: 120px; + border: none; + background: transparent; + outline: none; + font-size: 14px; + font-family: inherit; + color: ${cssManager.bdTheme('#333', '#fff')}; + padding: 4px; + } + + .tag-input::placeholder { + color: ${cssManager.bdTheme('#999', '#666')}; + } + + .tag-input:disabled { + cursor: not-allowed; + } + + /* Suggestions dropdown */ + .suggestions-container { + position: relative; + } + + .suggestions-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 4px; + background: ${cssManager.bdTheme('#ffffff', '#222222')}; + border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')}; + border-radius: 8px; + box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')}; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + } + + .suggestion { + padding: 8px 12px; + cursor: pointer; + transition: all 0.2s ease; + color: ${cssManager.bdTheme('#333', '#fff')}; + } + + .suggestion:hover, + .suggestion.highlighted { + background: ${cssManager.bdTheme('#f5f5f5', '#333333')}; + } + + .suggestion.highlighted { + background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; + } + + /* Validation styles */ + .validation-message { + color: #d32f2f; + font-size: 12px; + margin-top: 4px; + min-height: 16px; + } + + /* Description styles */ + .description { + color: ${cssManager.bdTheme('#666', '#999')}; + font-size: 12px; + margin-top: 4px; + } + `, + ]; + + public render(): TemplateResult { + const filteredSuggestions = this.suggestions.filter( + suggestion => + !this.value.includes(suggestion) && + suggestion.toLowerCase().includes(this.inputValue.toLowerCase()) + ); + + return html` +
+ ${this.label ? html`` : ''} + +
+
+ ${this.value.map(tag => html` +
+ ${tag} + ${!this.disabled ? html` +
this.removeTag(e, tag)}> + +
+ ` : ''} +
+ `)} + + ${!this.disabled && (!this.maxTags || this.value.length < this.maxTags) ? html` + + ` : ''} +
+ + ${this.showSuggestions && filteredSuggestions.length > 0 ? html` +
+ ${filteredSuggestions.map((suggestion, index) => html` +
{ + e.preventDefault(); // Prevent blur + this.addTag(suggestion); + }} + @mouseenter=${() => this.highlightedSuggestionIndex = index} + > + ${suggestion} +
+ `)} +
+ ` : ''} +
+ + ${this.validationText ? html` +
${this.validationText}
+ ` : ''} + + ${this.description ? html` +
${this.description}
+ ` : ''} +
+ `; + } + + private handleContainerClick(e: Event) { + if (this.disabled) return; + + const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement; + if (input && e.target !== input) { + input.focus(); + } + } + + private handleInput(e: Event) { + const input = e.target as HTMLInputElement; + this.inputValue = input.value; + + // Check for comma or semicolon to add tag + if (this.inputValue.includes(',') || this.inputValue.includes(';')) { + const tag = this.inputValue.replace(/[,;]/g, '').trim(); + if (tag) { + this.addTag(tag); + } + } + } + + private handleKeyDown(e: KeyboardEvent) { + const input = e.target as HTMLInputElement; + + if (e.key === 'Enter') { + e.preventDefault(); + if (this.highlightedSuggestionIndex >= 0 && this.showSuggestions) { + const filteredSuggestions = this.suggestions.filter( + suggestion => + !this.value.includes(suggestion) && + suggestion.toLowerCase().includes(this.inputValue.toLowerCase()) + ); + if (filteredSuggestions[this.highlightedSuggestionIndex]) { + this.addTag(filteredSuggestions[this.highlightedSuggestionIndex]); + } + } else if (this.inputValue.trim()) { + this.addTag(this.inputValue.trim()); + } + } else if (e.key === 'Backspace' && !this.inputValue && this.value.length > 0) { + // Remove last tag when backspace is pressed on empty input + this.removeTag(e, this.value[this.value.length - 1]); + } else if (e.key === 'ArrowDown' && this.showSuggestions) { + e.preventDefault(); + const filteredCount = this.suggestions.filter( + s => !this.value.includes(s) && s.toLowerCase().includes(this.inputValue.toLowerCase()) + ).length; + this.highlightedSuggestionIndex = Math.min( + this.highlightedSuggestionIndex + 1, + filteredCount - 1 + ); + } else if (e.key === 'ArrowUp' && this.showSuggestions) { + e.preventDefault(); + this.highlightedSuggestionIndex = Math.max(this.highlightedSuggestionIndex - 1, 0); + } else if (e.key === 'Escape') { + this.showSuggestions = false; + this.highlightedSuggestionIndex = -1; + } + } + + private handleFocus() { + if (this.suggestions.length > 0) { + this.showSuggestions = true; + } + } + + private handleBlur() { + // Delay to allow click on suggestions + setTimeout(() => { + this.showSuggestions = false; + this.highlightedSuggestionIndex = -1; + }, 200); + } + + private addTag(tag: string) { + if (!tag || this.value.includes(tag)) return; + if (this.maxTags && this.value.length >= this.maxTags) return; + + this.value = [...this.value, tag]; + this.inputValue = ''; + this.showSuggestions = false; + this.highlightedSuggestionIndex = -1; + + // Clear the input + const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement; + if (input) { + input.value = ''; + } + + this.emitChange(); + } + + private removeTag(e: Event, tag: string) { + e.stopPropagation(); + this.value = this.value.filter(t => t !== tag); + this.emitChange(); + } + + private emitChange() { + this.dispatchEvent(new CustomEvent('change', { + detail: { value: this.value }, + bubbles: true, + composed: true + })); + this.changeSubject.next(this); + } + + public getValue(): string[] { + return this.value; + } + + public setValue(value: string[]): void { + this.value = value || []; + } + + public async validate(): Promise { + if (this.required && (!this.value || this.value.length === 0)) { + this.validationText = 'At least one tag is required'; + return false; + } + this.validationText = ''; + return true; + } +} \ No newline at end of file diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index af856cb..3ad11b8 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -37,6 +37,7 @@ export * from './dees-progressbar.js'; export * from './dees-input-quantityselector.js'; export * from './dees-input-radiogroup.js'; export * from './dees-input-richtext.js'; +export * from './dees-input-tags.js'; export * from './dees-input-text.js'; export * from './dees-label.js'; export * from './dees-mobilenavigation.js'; diff --git a/ts_web/pages/input-showcase.ts b/ts_web/pages/input-showcase.ts index 424405e..f6387ed 100644 --- a/ts_web/pages/input-showcase.ts +++ b/ts_web/pages/input-showcase.ts @@ -377,6 +377,29 @@ export const inputShowcase = () => html` .placeholder=${'Type and press Enter'} > + + +
+ + + +
+
diff --git a/ts_web/pages/zindex-showcase.ts b/ts_web/pages/zindex-showcase.ts index 2054c40..62e8c71 100644 --- a/ts_web/pages/zindex-showcase.ts +++ b/ts_web/pages/zindex-showcase.ts @@ -8,6 +8,7 @@ import '../elements/dees-form.js'; import '../elements/dees-panel.js'; import '../elements/dees-input-text.js'; import '../elements/dees-input-radiogroup.js'; +import '../elements/dees-input-tags.js'; import '../elements/dees-appui-profiledropdown.js'; export const showcasePage = () => html` @@ -512,6 +513,13 @@ export const showcasePage = () => html` .label=${'Additional Field'} .placeholder=${'Just to show form context'} > + +

You can also right-click anywhere in this modal to test context menus. @@ -642,6 +650,47 @@ export const showcasePage = () => html` }}>Show Multiple Toasts +

+

Modal with Tags Input

+ { + await DeesModal.createAndShow({ + heading: 'Tags Input Test', + width: 'medium', + content: html` +

Test the tags input component in a modal:

+ + + + + + `, + menuOptions: [ + { name: 'Cancel', action: async (modal) => modal.destroy() }, + { name: 'Apply', action: async (modal) => { + DeesToast.createAndShow({ message: 'Tags applied!', type: 'success' }); + modal.destroy(); + }} + ] + }); + }}>Test Tags in Modal
+
+

Fullscreen Modal

{