@kubex/zinc 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/custom-elements.json +464 -34
  2. package/dist/vscode.html-custom-data.json +38 -6
  3. package/dist/web-types.json +97 -10
  4. package/dist/zn.d.ts +73 -8
  5. package/dist/zn.min.js +393 -349
  6. package/docs/data/products-table.json +203 -0
  7. package/docs/pages/components/button.md +25 -0
  8. package/docs/pages/components/data-table.md +21 -0
  9. package/docs/pages/components/header.md +7 -0
  10. package/docs/pages/components/order-table.md +82 -5
  11. package/package.json +1 -1
  12. package/src/components/button/button.component.ts +168 -10
  13. package/src/components/button/button.scss +115 -0
  14. package/src/components/content-block/content-block.component.ts +186 -107
  15. package/src/components/content-block/content-block.scss +4 -2
  16. package/src/components/data-select/data-select.component.ts +50 -10
  17. package/src/components/data-table/data-table.component.ts +130 -43
  18. package/src/components/data-table/data-table.scss +85 -7
  19. package/src/components/data-table-filter/data-table-filter.component.ts +1 -1
  20. package/src/components/editor/editor.scss +1 -0
  21. package/src/components/editor/modules/attachment/attachment.ts +53 -41
  22. package/src/components/editor/modules/toolbar/toolbar.ts +42 -0
  23. package/src/components/empty-state/empty-state.component.ts +16 -8
  24. package/src/components/expanding-action/expanding-action.component.ts +1 -1
  25. package/src/components/header/header.component.ts +8 -2
  26. package/src/components/header/header.scss +5 -1
  27. package/src/components/icon/icon.component.ts +2 -0
  28. package/src/components/icon/icon.scss +13 -0
  29. package/src/components/item/item.component.ts +11 -3
  30. package/src/components/item/item.scss +7 -4
  31. package/src/components/linked-select/linked-select.component.ts +1 -0
  32. package/src/components/menu/menu.component.ts +0 -1
  33. package/src/components/navbar/navbar.component.ts +1 -1
  34. package/src/components/note/note.component.ts +70 -2
  35. package/src/components/note/note.scss +26 -4
  36. package/src/components/order-table/order-table.component.ts +49 -28
  37. package/src/components/panel/panel.component.ts +5 -1
  38. package/src/components/panel/panel.scss +6 -0
  39. package/src/components/rating/rating.component.ts +10 -9
  40. package/src/components/rating/rating.scss +15 -1
@@ -23,6 +23,7 @@
23
23
  border-color: transparent;
24
24
  height: 100%;
25
25
  display: inline-flex;
26
+ position: relative;
26
27
 
27
28
  &.button--info {
28
29
  --btn-color: var(--zn-color-info);
@@ -39,6 +40,10 @@
39
40
  &.button--success {
40
41
  --btn-color: var(--zn-color-success);
41
42
  }
43
+
44
+ &__label--hidden {
45
+ visibility: hidden;
46
+ }
42
47
  }
43
48
 
44
49
  .button.button--muted-notification {
@@ -306,6 +311,116 @@
306
311
  }
307
312
  }
308
313
 
314
+ .button--loading-bg {
315
+ background-color: #e0e0e0;
316
+
317
+ &-transparent {
318
+ background-color: transparent;
319
+ }
320
+ }
321
+
322
+ .button--loading {
323
+ &-fill {
324
+ pointer-events: none;
325
+ position: absolute;
326
+ z-index: 1;
327
+ background-color: rgba(var(--btn-color), .85);
328
+ border-radius: inherit;
329
+ transition: width 0s;
330
+ animation: loading-fill-grow var(--loading-duration, 2s) linear forwards;
331
+ inset: -1px;
332
+
333
+ &-transparent {
334
+ background-color: rgba(var(--zn-primary), 0.1);
335
+ }
336
+ }
337
+
338
+ &-container {
339
+ position: absolute;
340
+ top: 0;
341
+ left: 0;
342
+ width: 100%;
343
+ height: 100%;
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ pointer-events: none;
348
+ }
349
+
350
+ &-text-bottom, &-text-top {
351
+ width: 100%;
352
+ height: 100%;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ font-size: inherit;
357
+ font-weight: inherit;
358
+ user-select: none;
359
+ position: absolute;
360
+ top: 0;
361
+ left: 0;
362
+ pointer-events: none;
363
+
364
+ .loading-countdown {
365
+ margin-left: 3px;
366
+ }
367
+
368
+ &-left {
369
+ justify-content: flex-start;
370
+ padding-inline-start: 7px;
371
+ }
372
+
373
+ &-right {
374
+ justify-content: flex-end;
375
+ padding-inline-end: 7px;
376
+ }
377
+ }
378
+
379
+ &-text-bottom {
380
+ z-index: 2;
381
+ color: rgba(var(--btn-color), .85);
382
+
383
+ &-transparent {
384
+ color: inherit;
385
+ }
386
+ }
387
+
388
+ &-text-top {
389
+ z-index: 3;
390
+ color: inherit;
391
+ /* Animate the reveal from left to right using a mask */
392
+ mask-image: linear-gradient(90deg, #000 0%, #000 100%);
393
+ mask-size: 0 100%;
394
+ mask-repeat: no-repeat;
395
+ animation: loading-text-reveal var(--loading-duration, 2s) linear forwards;
396
+ }
397
+ }
398
+
399
+ .button--relative-wrapper {
400
+ position: relative;
401
+ display: inline-flex;
402
+ height: 100%;
403
+ width: 100%;
404
+ }
405
+
406
+ @keyframes loading-fill-grow {
407
+ from {
408
+ width: 0;
409
+ }
410
+ to {
411
+ width: 100%;
412
+ }
413
+ }
414
+
415
+ @keyframes loading-text-reveal {
416
+ from {
417
+ mask-size: 0 100%;
418
+ }
419
+ to {
420
+ mask-size: 100% 100%;
421
+ }
422
+ }
423
+
309
424
  :host([vertical-align="center"]) {
310
425
  align-self: center;
311
426
  }
@@ -1,10 +1,12 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
+ import {deepQuerySelectorAll} from "../../utilities/query";
2
3
  import {HasSlotController} from "../../internal/slot";
3
- import {html, unsafeCSS} from 'lit';
4
+ import type {PropertyValues} from 'lit';
5
+ import {html, nothing, unsafeCSS} from 'lit';
4
6
  import {property, queryAssignedNodes, queryAsync} from 'lit/decorators.js';
5
7
  import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
6
8
  import ZincElement from "../../internal/zinc-element";
7
- import type {PropertyValues} from 'lit';
9
+ import type ZnTile from "../tile";
8
10
 
9
11
  import styles from './content-block.scss';
10
12
 
@@ -27,7 +29,11 @@ export default class ContentBlock extends ZincElement {
27
29
  @property() avatar = '';
28
30
 
29
31
  @property({type: Boolean, reflect: true}) outbound = false;
32
+
33
+ @property({type: Boolean, attribute: "no-collapse"}) noCollapse = false;
34
+
30
35
  @property({type: Boolean, reflect: true}) short = false;
36
+
31
37
  @property({attribute: 'default-display', reflect: true}) defaultDisplay: 'text' | 'html' = 'text';
32
38
 
33
39
  @queryAssignedNodes({slot: 'html', flatten: true}) htmlNodes!: Node[];
@@ -38,83 +44,53 @@ export default class ContentBlock extends ZincElement {
38
44
 
39
45
  private _textRows: TextRow[] = [];
40
46
 
41
- private _themeChangeObserver?: MutationObserver;
47
+ private _footerObserver?: MutationObserver;
48
+ private _replaceDebounce: number = 0;
42
49
 
43
50
  connectedCallback() {
44
51
  super.connectedCallback();
45
52
  this.iframe.then((iframe) => {
46
- const rebuild = () => {
53
+ if (iframe.contentDocument) {
47
54
  const div = this.htmlNodes[0] as HTMLDivElement;
48
- if (!div) return;
49
-
50
- let convertedHtml = div?.innerHTML ?? '';
51
- convertedHtml = convertedHtml.replace(/ /g, ' ');
52
- convertedHtml = convertedHtml.replace(/&lt;/g, '<');
53
- convertedHtml = convertedHtml.replace(/&gt;/g, '>');
54
- convertedHtml = convertedHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
55
-
56
- const themeAttr = document.documentElement.getAttribute('t') || document.body.getAttribute('t') || '';
57
-
58
- // Pull stylesheet links from host to inherit Zinc styles
59
- const linkTags = Array.from(document.head.querySelectorAll('link[rel="stylesheet"]'))
60
- .map((element: Element) => {
61
- const href = (element as HTMLLinkElement).getAttribute('href') || (element as HTMLLinkElement).href || '';
62
- return href ? `<link rel="stylesheet" href="${href}">` : '';
63
- })
64
- .join('');
65
-
66
- const baseStyles = `<style>
67
- html, body {
68
- background-color: var(--zn-panel-background-color);
69
- container-type: size;
70
- margin: 0;
71
- }
72
-
73
- hr {
74
- height: 0;
75
- border: none;
76
- border-top: 1px solid rgb(var(--zn-border-color));
77
- }
78
-
79
- code, kbd, samp, pre, .mono {
80
- font-family: var(--zn-font-family-mono),sans-serif;
81
- font-size: 1em;
82
- }
83
-
84
- small {
85
- font-size: 80%;
86
- }
87
- </style>`;
88
-
89
- const head = `${linkTags}${baseStyles}`;
90
- const htmlAttrs = `${themeAttr ? ` t="${themeAttr}"` : ''}`;
91
- const baseHref = document.baseURI || window.location.href;
92
-
93
- iframe.srcdoc = `<!doctype html><html${htmlAttrs}><head><base href="${baseHref}" target="_blank">${head}</head><body>${convertedHtml}</body></html>`;
94
- };
95
-
96
- rebuild();
97
-
98
- // Observe theme changes on both attribute and theme classes on html and body
99
- this._themeChangeObserver = new MutationObserver(() => rebuild());
100
- this._themeChangeObserver.observe(document.documentElement, {attributes: true, attributeFilter: ['t', 'class']});
101
- this._themeChangeObserver.observe(document.body, {attributes: true, attributeFilter: ['t', 'class']});
102
-
103
- const doc = iframe.contentDocument;
104
- if (!doc) return;
55
+ if (div) {
56
+ let convertedHtml = div?.innerHTML ?? '';
57
+ convertedHtml = convertedHtml.replace(/&nbsp;/g, ' ');
58
+ convertedHtml = convertedHtml.replace(/&lt;/g, '<');
59
+ convertedHtml = convertedHtml.replace(/&gt;/g, '>');
60
+ convertedHtml = convertedHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
61
+
62
+ const baseStyles = "<style>body,html{background-color: #ffffff}img{max-width:100%; max-height: 500px;}</style>";
63
+ iframe.srcdoc = baseStyles + convertedHtml;
64
+ }
65
+ }
105
66
 
106
- iframe.style.height = (75 + doc.body.scrollHeight) + 'px';
107
- iframe.style.width = '100%';
108
- iframe.style.border = '0';
67
+ iframe.addEventListener("load", () => {
68
+ setTimeout(() => this._resizeIframe(iframe), 50);
69
+ });
109
70
  });
110
71
  }
111
72
 
112
73
  disconnectedCallback() {
113
- super.disconnectedCallback();
114
- this._themeChangeObserver?.disconnect();
74
+ try {
75
+ if (this._footerObserver) {
76
+ this._footerObserver.disconnect();
77
+ this._footerObserver = undefined;
78
+ }
79
+ if (this._replaceDebounce) {
80
+ clearTimeout(this._replaceDebounce);
81
+ this._replaceDebounce = 0;
82
+ }
83
+ } finally {
84
+ // Ensure base class cleanup
85
+ // @ts-ignore - base may or may not implement disconnectedCallback
86
+ super.disconnectedCallback?.();
87
+ }
115
88
  }
116
89
 
117
90
  private _collapseContent(e: Event) {
91
+ if (this.noCollapse) {
92
+ return;
93
+ }
118
94
  const target = e.target as HTMLElement;
119
95
  const headerClick = target.classList.contains('content-block-header') ||
120
96
  target.parentElement?.classList.contains('content-block-header');
@@ -122,6 +98,15 @@ export default class ContentBlock extends ZincElement {
122
98
  if (headerClick && target.slot !== 'nav') {
123
99
  this.short = !this.short;
124
100
  this.requestUpdate();
101
+ this.updateComplete.then(() => {
102
+ if (!this.short) {
103
+ this.iframe.then((iframe) => {
104
+ if (iframe && !iframe.classList.contains('hidden')) {
105
+ this._resizeIframe(iframe);
106
+ }
107
+ });
108
+ }
109
+ });
125
110
  }
126
111
  }
127
112
 
@@ -141,10 +126,46 @@ export default class ContentBlock extends ZincElement {
141
126
  if (textContent) {
142
127
  iframe.classList.remove('hidden');
143
128
  textContent.classList.add('hidden');
129
+ setTimeout(() => this._resizeIframe(iframe), 0);
144
130
  }
145
131
  });
146
132
  }
147
133
 
134
+ private _resizeIframe(iframe?: HTMLIFrameElement) {
135
+ const resize = (frame: HTMLIFrameElement) => {
136
+ const doc = frame.contentDocument;
137
+ if (!doc) return;
138
+
139
+ // Allow the iframe to shrink as well as grow
140
+ frame.style.height = 'auto';
141
+
142
+ const body = doc.body;
143
+ const element = doc.documentElement;
144
+
145
+ const heights = [
146
+ body?.scrollHeight ?? 0,
147
+ element?.scrollHeight ?? 0,
148
+ body?.offsetHeight ?? 0,
149
+ element?.offsetHeight ?? 0
150
+ ];
151
+ const height = Math.max(...heights);
152
+
153
+ if (height > 0) {
154
+ frame.style.height = `${height}px`;
155
+ }
156
+ };
157
+
158
+ if (iframe) {
159
+ resize(iframe);
160
+ } else {
161
+ this.iframe.then((frame) => {
162
+ if (frame?.contentDocument) {
163
+ requestAnimationFrame(() => resize(frame));
164
+ }
165
+ });
166
+ }
167
+ }
168
+
148
169
  protected firstUpdated(_changedProperties: PropertyValues) {
149
170
  super.firstUpdated(_changedProperties);
150
171
 
@@ -167,22 +188,29 @@ export default class ContentBlock extends ZincElement {
167
188
  const initialShowHtml = hasHtmlSlot && (!hasTextSlot || this.defaultDisplay === 'html');
168
189
 
169
190
  return html`
170
- <zn-panel flush tabbed class="${classMap({
171
- 'content-block--outbound': this.outbound,
172
- 'content-block--short': this.short
173
- })}">
191
+ <zn-panel flush
192
+ tabbed
193
+ flush-footer="${hasFooter || nothing}"
194
+ class="${classMap({
195
+ 'content-block--outbound': this.outbound,
196
+ 'content-block--short': this.short,
197
+ 'content-block--collapsible': !this.noCollapse
198
+ })}">
174
199
 
175
200
  <zn-header class="content-block-header"
176
- caption="${this.sender}"
177
201
  description="${this.truncateText()}"
178
202
  @click="${this._collapseContent}">
179
203
 
180
- <zn-icon src="${this.avatar}"
181
- library="avatar"
182
- round></zn-icon>
204
+ <slot name="icon">
205
+ <zn-icon src="${this.avatar}"
206
+ library="avatar"
207
+ round></zn-icon>
208
+ </slot>
209
+
210
+ <slot name="caption" slot="caption">${this.sender}</slot>
183
211
 
184
212
  <div slot="actions">
185
- <small>${this.time}</small>
213
+ <slot name="actions"><small>${this.time}</small></slot>
186
214
  ${showActions ? html`
187
215
  <zn-dropdown>
188
216
  <zn-button slot="trigger" icon="more_vert" icon-size="24" color="transparent"
@@ -222,7 +250,7 @@ export default class ContentBlock extends ZincElement {
222
250
  </zn-sp>
223
251
 
224
252
  ${hasFooter ? html`
225
- <slot slot="footer" name="footer"></slot>` : ''}
253
+ <slot slot="footer" name="footer" @slotchange="${this._handleSlotChange}"></slot>` : ''}
226
254
  </zn-panel>
227
255
  `;
228
256
  }
@@ -234,6 +262,88 @@ export default class ContentBlock extends ZincElement {
234
262
  return trimmed.length > 32 ? trimmed.substring(0, 32) + '...' : trimmed;
235
263
  }
236
264
 
265
+ private _handleSlotChange(e: Event) {
266
+ const slot = e.target as HTMLSlotElement;
267
+ const assignedEls: Element[] = slot.assignedElements({flatten: true});
268
+ if (!assignedEls || assignedEls.length === 0) return;
269
+
270
+ // Reset and attach a new observer to watch for dynamic content inside the footer subtree
271
+ if (this._footerObserver) {
272
+ this._footerObserver.disconnect();
273
+ this._footerObserver = undefined;
274
+ }
275
+
276
+ const obs = new MutationObserver(() => {
277
+ this._debouncedReplace();
278
+ });
279
+ this._footerObserver = obs;
280
+
281
+ assignedEls.forEach(el => {
282
+ obs.observe(el, {
283
+ subtree: true,
284
+ childList: true,
285
+ attributes: true,
286
+ attributeFilter: ['href', 'download', 'caption']
287
+ });
288
+ });
289
+
290
+ // Run now and again shortly after to catch async rendering
291
+ this._replaceImagePlaceholders();
292
+ setTimeout(() => this._replaceImagePlaceholders(), 50);
293
+ }
294
+
295
+ private _debouncedReplace() {
296
+ if (this._replaceDebounce) {
297
+ clearTimeout(this._replaceDebounce);
298
+ }
299
+ this._replaceDebounce = window.setTimeout(() => this._replaceImagePlaceholders(), 50);
300
+ }
301
+
302
+ private _replaceImagePlaceholders() {
303
+ const textContainer = this.shadowRoot?.querySelector('.text-content') as HTMLDivElement | null;
304
+ if (!textContainer) {
305
+ // If the shadow content hasn't been rendered yet, try after the next update
306
+ this.updateComplete.then(() => this._replaceImagePlaceholders());
307
+ return;
308
+ }
309
+
310
+ const footer = this.querySelector(':scope > [slot="footer"]') as HTMLElement | null;
311
+ if (!footer) return;
312
+
313
+ const anchors = deepQuerySelectorAll('a[href]', footer, '') as HTMLAnchorElement[];
314
+ if (anchors.length === 0) return;
315
+
316
+ let textContent = textContainer.innerHTML;
317
+ const extensions = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'heic', 'tiff', 'tif', 'avif']);
318
+ anchors.forEach((a) => {
319
+ const candidates = new Set<string>();
320
+ const tile = a.querySelector('zn-tile') as ZnTile | null;
321
+
322
+ const caption = (tile?.caption ?? tile?.getAttribute?.('caption') ?? '').trim();
323
+ if (caption) candidates.add(caption);
324
+
325
+ const downloadAttr = (a.getAttribute('download') || '').trim();
326
+ if (downloadAttr) candidates.add(downloadAttr);
327
+
328
+ const url = a.getAttribute('href') || '';
329
+ candidates.forEach((rawName) => {
330
+ const name = (rawName || '').trim();
331
+ if (!name) return;
332
+
333
+ const base = name.includes('/') ? name.split('/').pop()! : name;
334
+ const lowerBase = base.toLowerCase();
335
+ const ext = lowerBase.includes('.') ? lowerBase.split('.').pop()! : '';
336
+ if (ext && !extensions.has(ext)) return;
337
+
338
+ const alt = base.replace(/"/g, '&quot;');
339
+ textContent = textContent.replace(`[image: ${base}]`, () => `<img src="${url}" alt="${alt}" style="max-height: 500px; max-width: 100%">`);
340
+ });
341
+ });
342
+
343
+ // Update the generated content directly without triggering a re-render
344
+ textContainer.innerHTML = textContent;
345
+ }
346
+
237
347
  protected getTextSections(): TextRow[] {
238
348
  const textContent = this.querySelector('[slot="text"]') as HTMLDivElement | null;
239
349
  const textRows: TextRow[] = [];
@@ -255,7 +365,6 @@ export default class ContentBlock extends ZincElement {
255
365
  if (row.startsWith('Sent from')) type = 'reply';
256
366
  if (row.trim() === '' && previousType === 'reply') type = 'reply';
257
367
  if (type === 'reply' && row.startsWith('>')) row = row.substring(1).trim();
258
- row = this.transformLineForImages(row);
259
368
 
260
369
  if (previousType === type) {
261
370
  if (textRows.length === 0) {
@@ -275,36 +384,6 @@ export default class ContentBlock extends ZincElement {
275
384
  return this._textRows;
276
385
  }
277
386
 
278
- private transformLineForImages(line: string): string {
279
- let out = line;
280
-
281
- // Replace Markdown image syntax ![alt](url) for http(s)
282
- const mdHttpImg = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+?\.(?:png|jpe?g|gif|webp|bmp|svg))\)/gi;
283
- out = out.replace(mdHttpImg, (_match, alt, url) => {
284
- return `<img src="${url}" alt="${alt || 'image'}" loading="lazy" style="max-width:100%;max-height:500px;">`;
285
- });
286
-
287
- // Replace Markdown image syntax with data URI (base64)
288
- const mdDataImg = /!\[([^\]]*)\]\((data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+)\)/gi;
289
- out = out.replace(mdDataImg, (_match, alt, url) => {
290
- return `<img src="${url}" alt="${alt || 'image'}" loading="lazy" style="max-width:100%;max-height:500px;">`;
291
- });
292
-
293
- // Replace bare http(s) image URLs with <img>
294
- const httpImg = /(https?:\/\/[^\s]+?\.(?:png|jpe?g|gif|webp|bmp|svg))(?!\S)/gi;
295
- out = out.replace(httpImg, (_m, url) => {
296
- return `<img src="${url}" alt="image" loading="lazy" style="max-width:100%;max-height:500px;">`;
297
- });
298
-
299
- // Replace bare data URI images with <img>
300
- const dataImg = /(data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+)(?!\S)/gi;
301
- out = out.replace(dataImg, (_m, url) => {
302
- return `<img src="${url}" alt="image" loading="lazy" style="max-width:100%;max-height:500px;">`;
303
- });
304
-
305
- return out;
306
- }
307
-
308
387
  showReply(e: MouseEvent) {
309
388
  const target = e.target as HTMLElement;
310
389
  const nextElement = target.nextElementSibling;
@@ -19,8 +19,10 @@ zn-header div[slot='actions'] {
19
19
  gap: 10px;
20
20
  }
21
21
 
22
- zn-header::part(base) {
23
- cursor: pointer;
22
+ .content-block--collapsible {
23
+ zn-header::part(base) {
24
+ cursor: pointer;
25
+ }
24
26
  }
25
27
 
26
28
  zn-header::part(header-left) {
@@ -7,13 +7,12 @@ import {
7
7
  emptyDataProvider,
8
8
  type LocalDataProvider,
9
9
  } from "./providers/provider";
10
+ import {type CSSResultGroup, html, nothing, unsafeCSS, PropertyValues} from 'lit';
10
11
  import {FormControlController} from "../../internal/form";
11
- import {html, nothing, unsafeCSS} from 'lit';
12
12
  import {property, query} from 'lit/decorators.js';
13
13
  import {watch} from "../../internal/watch";
14
- import ZincElement from '../../internal/zinc-element';
15
- import type {CSSResultGroup} from 'lit';
16
14
  import type {ZincFormControl} from '../../internal/zinc-element';
15
+ import ZincElement from '../../internal/zinc-element';
17
16
  import type ZnSelect from "../select";
18
17
 
19
18
  import styles from './data-select.scss';
@@ -37,15 +36,22 @@ import styles from './data-select.scss';
37
36
  */
38
37
  export default class ZnDataSelect extends ZincElement implements ZincFormControl {
39
38
  static styles: CSSResultGroup = unsafeCSS(styles);
39
+
40
40
  @query('#select') select: ZnSelect;
41
41
  @query('#select__prefix') selectPrefix: HTMLElement;
42
+
42
43
  /** The name of the select. Used for form submission. */
43
44
  @property() name: string;
45
+
44
46
  /** The value of the select. Used for form submission. */
45
47
  @property() value: string;
48
+
46
49
  /** The provider of the select. */
47
50
  @property() provider: 'color' | 'currency' | 'country' | 'phone';
51
+
52
+ /** The position of the icon. */
48
53
  @property({attribute: 'icon-position'}) iconPosition: 'start' | 'end' | 'none' = 'none';
54
+
49
55
  /** An array of keys to use for filtering the options in the selected provider. */
50
56
  @property({
51
57
  attribute: 'filter',
@@ -54,27 +60,39 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
54
60
  toAttribute: (value: string[]) => value.join(',')
55
61
  }
56
62
  }) filter: string[];
63
+
57
64
  /** The selects size. */
58
65
  @property({reflect: true}) size: 'small' | 'medium' | 'large' = 'medium';
66
+
59
67
  /** Should we show the clear button */
60
68
  @property({type: Boolean}) clearable: boolean;
69
+
61
70
  /** Include an "All" option at the top. */
62
71
  @property({type: Boolean, attribute: 'allow-all'}) allowAll = false;
72
+
63
73
  /** The selects label. If you need to display HTML, use the `label` slot instead. */
64
74
  @property() label = '';
75
+
65
76
  /** Text that appears in a tooltip next to the label. If you need to display HTML in the tooltip, use the `label-tooltip` slot instead. */
66
77
  @property({attribute: 'label-tooltip'}) labelTooltip = '';
78
+
67
79
  /** Text that appears above the input, on the right, to add additional context. If you need to display HTML in this text, use the `context-note` slot instead. */
68
80
  @property({attribute: 'context-note'}) contextNote = '';
81
+
69
82
  /**
70
83
  * The preferred placement of the selects menu. Note that the actual placement may vary as needed to keep the listbox
71
84
  * inside the viewport.
72
85
  */
73
86
  @property({reflect: true}) placement: 'top' | 'bottom' = 'bottom';
87
+
74
88
  /** The selects help text. If you need to display HTML, use the `help-text` slot instead. */
75
89
  @property({attribute: 'help-text'}) helpText = '';
90
+
76
91
  /** The selects required attribute. */
77
92
  @property({type: Boolean, reflect: true}) required = false;
93
+
94
+ @property({attribute: "icon-only", type: Boolean, reflect: true}) iconOnly = false;
95
+
78
96
  protected readonly formControlController = new FormControlController(this);
79
97
 
80
98
  get validationMessage() {
@@ -85,6 +103,13 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
85
103
  return this.select.validity;
86
104
  }
87
105
 
106
+ constructor() {
107
+ super();
108
+ if (this.iconOnly && this.iconPosition === 'none') {
109
+ this.iconPosition = 'start';
110
+ }
111
+ }
112
+
88
113
  connectedCallback() {
89
114
  super.connectedCallback();
90
115
  window.addEventListener('keydown', this.closeOnTab);
@@ -95,6 +120,11 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
95
120
  window.removeEventListener('keydown', this.closeOnTab);
96
121
  }
97
122
 
123
+ protected firstUpdated(_changedProperties: PropertyValues) {
124
+ super.firstUpdated(_changedProperties);
125
+ this._updatePrefix();
126
+ }
127
+
98
128
  checkValidity(): boolean {
99
129
  return this.select.checkValidity();
100
130
  }
@@ -185,15 +215,15 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
185
215
  @zn-clear="${this.handleClear}"
186
216
  @blur=${this.blur}
187
217
  value="${this.value}"
188
- placeholder="Choose a ${localProvider.getName}"
218
+ placeholder="${this.getPlaceholder(localProvider)}"
189
219
  exportparts="combobox,expand-icon,form-control-help-text,form-control-input,display-input">
190
- ${this.iconPosition !== 'none' ? html`
220
+ ${(this.iconPosition !== 'none' || this.iconOnly) ? html`
191
221
  <div id="select__prefix" slot="prefix" class="select__prefix"></div>` : ''}
192
222
  ${data.map((item: DataProviderOption) => html`
193
223
  <zn-option class="select__option" value="${item.key}">
194
- ${this.iconPosition !== 'none' ? html`<span
224
+ ${(this.iconPosition !== 'none' || this.iconOnly) ? html`<span
195
225
  slot="${this.iconPosition === 'end' ? 'suffix' : 'prefix'}">${item.prefix}</span>` : ''}
196
- ${item.value}
226
+ ${this.iconOnly ? undefined : item.value}
197
227
  </zn-option>`)}
198
228
  </zn-select>`;
199
229
  }
@@ -201,19 +231,29 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
201
231
  private _updatePrefix() {
202
232
  // Set the prefix of the select to the selected values prefix
203
233
  const selectedOption = this.select.selectedOptions[0];
204
- if (selectedOption && (this.iconPosition !== 'none')) {
234
+ if (selectedOption && (this.iconPosition !== 'none' || this.iconOnly) && this.selectPrefix) {
235
+ if (this.iconPosition === 'none' && this.iconOnly) {
236
+ this.iconPosition = 'start';
237
+ }
205
238
  const slot = this.iconPosition === 'start'
206
239
  ? selectedOption.querySelector('[slot="prefix"]')
207
240
  : selectedOption.querySelector('[slot="suffix"]');
208
-
209
241
  if (slot) {
210
242
  this.selectPrefix.innerHTML = '';
211
243
  this.selectPrefix.appendChild(slot.cloneNode(true));
212
244
  } else {
213
245
  this.selectPrefix.innerHTML = '';
214
246
  }
215
- } else {
247
+ } else if (this.selectPrefix) {
216
248
  this.selectPrefix.innerHTML = '';
217
249
  }
218
250
  }
251
+
252
+ private getPlaceholder(localProvider: LocalDataProvider<any>) {
253
+ if (this.value) {
254
+ return '';
255
+ }
256
+
257
+ return `Choose a ${localProvider.getName}`
258
+ }
219
259
  }