@kubex/zinc 1.0.6 → 1.0.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -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 {html, nothing, unsafeCSS} from 'lit';
4
5
  import {property, queryAssignedNodes, queryAsync} from 'lit/decorators.js';
5
6
  import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
6
7
  import ZincElement from "../../internal/zinc-element";
7
8
  import type {PropertyValues} from 'lit';
9
+ import type ZnTile from "../tile";
8
10
 
9
11
  import styles from './content-block.scss';
10
12
 
@@ -38,80 +40,47 @@ export default class ContentBlock extends ZincElement {
38
40
 
39
41
  private _textRows: TextRow[] = [];
40
42
 
41
- private _themeChangeObserver?: MutationObserver;
43
+ private _footerObserver?: MutationObserver;
44
+ private _replaceDebounce: number = 0;
42
45
 
43
46
  connectedCallback() {
44
47
  super.connectedCallback();
45
48
  this.iframe.then((iframe) => {
46
- const rebuild = () => {
49
+ if (iframe.contentDocument) {
47
50
  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;
51
+ if (div) {
52
+ let convertedHtml = div?.innerHTML ?? '';
53
+ convertedHtml = convertedHtml.replace(/&nbsp;/g, ' ');
54
+ convertedHtml = convertedHtml.replace(/&lt;/g, '<');
55
+ convertedHtml = convertedHtml.replace(/&gt;/g, '>');
56
+ convertedHtml = convertedHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
57
+
58
+ const baseStyles = "<style>body,html{background-color: #ffffff}img{max-width:100%; max-height: 500px;}</style>";
59
+ iframe.srcdoc = baseStyles + convertedHtml;
60
+ }
61
+ }
105
62
 
106
- iframe.style.height = (75 + doc.body.scrollHeight) + 'px';
107
- iframe.style.width = '100%';
108
- iframe.style.border = '0';
63
+ iframe.addEventListener("load", () => {
64
+ setTimeout(() => this._resizeIframe(iframe), 50);
65
+ });
109
66
  });
110
67
  }
111
68
 
112
69
  disconnectedCallback() {
113
- super.disconnectedCallback();
114
- this._themeChangeObserver?.disconnect();
70
+ try {
71
+ if (this._footerObserver) {
72
+ this._footerObserver.disconnect();
73
+ this._footerObserver = undefined;
74
+ }
75
+ if (this._replaceDebounce) {
76
+ clearTimeout(this._replaceDebounce);
77
+ this._replaceDebounce = 0;
78
+ }
79
+ } finally {
80
+ // Ensure base class cleanup
81
+ // @ts-ignore - base may or may not implement disconnectedCallback
82
+ super.disconnectedCallback?.();
83
+ }
115
84
  }
116
85
 
117
86
  private _collapseContent(e: Event) {
@@ -122,6 +91,15 @@ export default class ContentBlock extends ZincElement {
122
91
  if (headerClick && target.slot !== 'nav') {
123
92
  this.short = !this.short;
124
93
  this.requestUpdate();
94
+ this.updateComplete.then(() => {
95
+ if (!this.short) {
96
+ this.iframe.then((iframe) => {
97
+ if (iframe && !iframe.classList.contains('hidden')) {
98
+ this._resizeIframe(iframe);
99
+ }
100
+ });
101
+ }
102
+ });
125
103
  }
126
104
  }
127
105
 
@@ -141,10 +119,46 @@ export default class ContentBlock extends ZincElement {
141
119
  if (textContent) {
142
120
  iframe.classList.remove('hidden');
143
121
  textContent.classList.add('hidden');
122
+ setTimeout(() => this._resizeIframe(iframe), 0);
144
123
  }
145
124
  });
146
125
  }
147
126
 
127
+ private _resizeIframe(iframe?: HTMLIFrameElement) {
128
+ const resize = (frame: HTMLIFrameElement) => {
129
+ const doc = frame.contentDocument;
130
+ if (!doc) return;
131
+
132
+ // Allow the iframe to shrink as well as grow
133
+ frame.style.height = 'auto';
134
+
135
+ const body = doc.body;
136
+ const element = doc.documentElement;
137
+
138
+ const heights = [
139
+ body?.scrollHeight ?? 0,
140
+ element?.scrollHeight ?? 0,
141
+ body?.offsetHeight ?? 0,
142
+ element?.offsetHeight ?? 0
143
+ ];
144
+ const height = Math.max(...heights);
145
+
146
+ if (height > 0) {
147
+ frame.style.height = `${height}px`;
148
+ }
149
+ };
150
+
151
+ if (iframe) {
152
+ resize(iframe);
153
+ } else {
154
+ this.iframe.then((frame) => {
155
+ if (frame?.contentDocument) {
156
+ requestAnimationFrame(() => resize(frame));
157
+ }
158
+ });
159
+ }
160
+ }
161
+
148
162
  protected firstUpdated(_changedProperties: PropertyValues) {
149
163
  super.firstUpdated(_changedProperties);
150
164
 
@@ -167,10 +181,13 @@ export default class ContentBlock extends ZincElement {
167
181
  const initialShowHtml = hasHtmlSlot && (!hasTextSlot || this.defaultDisplay === 'html');
168
182
 
169
183
  return html`
170
- <zn-panel flush tabbed class="${classMap({
171
- 'content-block--outbound': this.outbound,
172
- 'content-block--short': this.short
173
- })}">
184
+ <zn-panel flush
185
+ tabbed
186
+ flush-footer="${hasFooter || nothing}"
187
+ class="${classMap({
188
+ 'content-block--outbound': this.outbound,
189
+ 'content-block--short': this.short
190
+ })}">
174
191
 
175
192
  <zn-header class="content-block-header"
176
193
  caption="${this.sender}"
@@ -222,7 +239,7 @@ export default class ContentBlock extends ZincElement {
222
239
  </zn-sp>
223
240
 
224
241
  ${hasFooter ? html`
225
- <slot slot="footer" name="footer"></slot>` : ''}
242
+ <slot slot="footer" name="footer" @slotchange="${this._handleSlotChange}"></slot>` : ''}
226
243
  </zn-panel>
227
244
  `;
228
245
  }
@@ -234,6 +251,88 @@ export default class ContentBlock extends ZincElement {
234
251
  return trimmed.length > 32 ? trimmed.substring(0, 32) + '...' : trimmed;
235
252
  }
236
253
 
254
+ private _handleSlotChange(e: Event) {
255
+ const slot = e.target as HTMLSlotElement;
256
+ const assignedEls: Element[] = slot.assignedElements({flatten: true});
257
+ if (!assignedEls || assignedEls.length === 0) return;
258
+
259
+ // Reset and attach a new observer to watch for dynamic content inside the footer subtree
260
+ if (this._footerObserver) {
261
+ this._footerObserver.disconnect();
262
+ this._footerObserver = undefined;
263
+ }
264
+
265
+ const obs = new MutationObserver(() => {
266
+ this._debouncedReplace();
267
+ });
268
+ this._footerObserver = obs;
269
+
270
+ assignedEls.forEach(el => {
271
+ obs.observe(el, {
272
+ subtree: true,
273
+ childList: true,
274
+ attributes: true,
275
+ attributeFilter: ['href', 'download', 'caption']
276
+ });
277
+ });
278
+
279
+ // Run now and again shortly after to catch async rendering
280
+ this._replaceImagePlaceholders();
281
+ setTimeout(() => this._replaceImagePlaceholders(), 50);
282
+ }
283
+
284
+ private _debouncedReplace() {
285
+ if (this._replaceDebounce) {
286
+ clearTimeout(this._replaceDebounce);
287
+ }
288
+ this._replaceDebounce = window.setTimeout(() => this._replaceImagePlaceholders(), 50);
289
+ }
290
+
291
+ private _replaceImagePlaceholders() {
292
+ const textContainer = this.shadowRoot?.querySelector('.text-content') as HTMLDivElement | null;
293
+ if (!textContainer) {
294
+ // If the shadow content hasn't been rendered yet, try after the next update
295
+ this.updateComplete.then(() => this._replaceImagePlaceholders());
296
+ return;
297
+ }
298
+
299
+ const footer = this.querySelector(':scope > [slot="footer"]') as HTMLElement | null;
300
+ if (!footer) return;
301
+
302
+ const anchors = deepQuerySelectorAll('a[href]', footer, '') as HTMLAnchorElement[];
303
+ if (anchors.length === 0) return;
304
+
305
+ let textContent = textContainer.innerHTML;
306
+ const extensions = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'heic', 'tiff', 'tif', 'avif']);
307
+ anchors.forEach((a) => {
308
+ const candidates = new Set<string>();
309
+ const tile = a.querySelector('zn-tile') as ZnTile | null;
310
+
311
+ const caption = (tile?.caption ?? tile?.getAttribute?.('caption') ?? '').trim();
312
+ if (caption) candidates.add(caption);
313
+
314
+ const downloadAttr = (a.getAttribute('download') || '').trim();
315
+ if (downloadAttr) candidates.add(downloadAttr);
316
+
317
+ const url = a.getAttribute('href') || '';
318
+ candidates.forEach((rawName) => {
319
+ const name = (rawName || '').trim();
320
+ if (!name) return;
321
+
322
+ const base = name.includes('/') ? name.split('/').pop()! : name;
323
+ const lowerBase = base.toLowerCase();
324
+ const ext = lowerBase.includes('.') ? lowerBase.split('.').pop()! : '';
325
+ if (ext && !extensions.has(ext)) return;
326
+
327
+ const alt = base.replace(/"/g, '&quot;');
328
+ textContent = textContent.replace(`[image: ${base}]`, () => `<img src="${url}" alt="${alt}" style="max-height: 500px; max-width: 100%">`);
329
+ });
330
+ });
331
+
332
+ // Update the generated content directly without triggering a re-render
333
+ textContainer.innerHTML = textContent;
334
+ }
335
+
237
336
  protected getTextSections(): TextRow[] {
238
337
  const textContent = this.querySelector('[slot="text"]') as HTMLDivElement | null;
239
338
  const textRows: TextRow[] = [];
@@ -255,7 +354,6 @@ export default class ContentBlock extends ZincElement {
255
354
  if (row.startsWith('Sent from')) type = 'reply';
256
355
  if (row.trim() === '' && previousType === 'reply') type = 'reply';
257
356
  if (type === 'reply' && row.startsWith('>')) row = row.substring(1).trim();
258
- row = this.transformLineForImages(row);
259
357
 
260
358
  if (previousType === type) {
261
359
  if (textRows.length === 0) {
@@ -275,36 +373,6 @@ export default class ContentBlock extends ZincElement {
275
373
  return this._textRows;
276
374
  }
277
375
 
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
376
  showReply(e: MouseEvent) {
309
377
  const target = e.target as HTMLElement;
310
378
  const nextElement = target.nextElementSibling;
@@ -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,30 @@ 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
+ console.log('selectedOption', selectedOption);
235
+ if (selectedOption && (this.iconPosition !== 'none' || this.iconOnly) && this.selectPrefix) {
236
+ if (this.iconPosition === 'none' && this.iconOnly) {
237
+ this.iconPosition = 'start';
238
+ }
205
239
  const slot = this.iconPosition === 'start'
206
240
  ? selectedOption.querySelector('[slot="prefix"]')
207
241
  : selectedOption.querySelector('[slot="suffix"]');
208
-
209
242
  if (slot) {
210
243
  this.selectPrefix.innerHTML = '';
211
244
  this.selectPrefix.appendChild(slot.cloneNode(true));
212
245
  } else {
213
246
  this.selectPrefix.innerHTML = '';
214
247
  }
215
- } else {
248
+ } else if (this.selectPrefix) {
216
249
  this.selectPrefix.innerHTML = '';
217
250
  }
218
251
  }
252
+
253
+ private getPlaceholder(localProvider: LocalDataProvider<any>) {
254
+ if (this.value) {
255
+ return '';
256
+ }
257
+
258
+ return `Choose a ${localProvider.getName}`
259
+ }
219
260
  }
@@ -184,7 +184,7 @@ export default class ZnDataTable extends ZincElement {
184
184
  @property({attribute: "empty-state-icon"}) emptyStateIcon: string = "data_alert";
185
185
 
186
186
  // Hide the checkbox column
187
- @property({attribute: 'hide-checkboxes', type: Boolean}) hideCheckboxes: boolean;
187
+ @property({attribute: 'hide-checkboxes', type: Boolean}) hideCheckboxes: boolean = true;
188
188
 
189
189
  @property() filters: [] = [];
190
190
 
@@ -278,8 +278,6 @@ export default class ZnDataTable extends ZincElement {
278
278
 
279
279
  requestParams: Record<string, any> = {};
280
280
 
281
- builtinRequestParams: Record<string, any> = {};
282
-
283
281
  refresh() {
284
282
  // Allow manual refresh to trigger the first data load when no-initial-load is set
285
283
  this._initialLoad = false;
@@ -424,7 +422,6 @@ export default class ZnDataTable extends ZincElement {
424
422
  'table': true,
425
423
  'table--standalone': this.standalone,
426
424
  'with-hover': !this.unsortable,
427
- 'table--with-checkboxes': !this.hideCheckboxes,
428
425
  })}">
429
426
  <thead>
430
427
  <tr>
@@ -439,11 +436,7 @@ export default class ZnDataTable extends ZincElement {
439
436
  </thead>
440
437
  <tbody>
441
438
  ${this._rows.map((row: Row) => html`
442
- <tr>
443
- ${this.hideCheckboxes ? html`` : html`
444
- <td class="${classMap({'hidden': !hasSelectedRows})}">
445
- <div><input type="checkbox" @change="${this.selectRow}"></div>
446
- </td>`}
439
+ <tr class="${classMap({'table__row--selected': this.isRowSelected(row)})}">
447
440
  ${row.cells.map((value: Cell, index: number) => this.renderCellBody(index, value))}
448
441
  ${this.rowHasActions ? this.renderActions(row) : null}
449
442
  </tr>`)}
@@ -490,7 +483,7 @@ export default class ZnDataTable extends ZincElement {
490
483
  }
491
484
 
492
485
  getRowsSelected() {
493
- if (this.hideCheckboxes || this.selectedRows.length <= 0) return null;
486
+ if (this.selectedRows.length <= 0) return null;
494
487
 
495
488
  return html`
496
489
  <p>${this.numberOfRowsSelected} of ${this._rows.length} rows selected</p>`
@@ -624,7 +617,7 @@ export default class ZnDataTable extends ZincElement {
624
617
  const checked = checkbox.checked;
625
618
 
626
619
  // go through all the checkboxes and check them
627
- for (const row of this.renderRoot.querySelectorAll('tbody input[type="checkbox"]')) {
620
+ for (const row of this.renderRoot.querySelectorAll('tbody zn-checkbox')) {
628
621
  (row as HTMLInputElement).checked = checked;
629
622
  }
630
623
 
@@ -655,17 +648,19 @@ export default class ZnDataTable extends ZincElement {
655
648
  return;
656
649
  }
657
650
 
658
- const checkbox: HTMLInputElement | null = parent.querySelector('input[type="checkbox"]');
659
- if (checkbox) {
660
- const isCheckboxTarget = target instanceof HTMLInputElement && target.type === 'checkbox';
661
- if (!isCheckboxTarget) {
662
- checkbox.checked = !checkbox.checked;
663
- }
664
- }
651
+ const rows = Array.from(this.renderRoot.querySelectorAll('tbody tr'));
652
+ const index = rows.indexOf(parent as HTMLTableRowElement);
653
+ if (index === -1) return;
665
654
 
666
- this.selectedRows = this._rows.filter((_, index) => {
667
- return (this.renderRoot.querySelectorAll('tbody input[type="checkbox"]')[index] as HTMLInputElement)?.checked;
668
- });
655
+ const row = this._rows[index] as Row;
656
+ if (!row) return;
657
+
658
+ const alreadySelected = this.selectedRows.some((r: Row) => r.id === row.id);
659
+ if (alreadySelected) {
660
+ this.selectedRows = this.selectedRows.filter((r: Row) => r.id !== row.id);
661
+ } else {
662
+ this.selectedRows = [...(this.selectedRows as Row[]), row];
663
+ }
669
664
 
670
665
  this.numberOfRowsSelected = this.selectedRows.length;
671
666
  this.updateKeys();
@@ -676,13 +671,9 @@ export default class ZnDataTable extends ZincElement {
676
671
  const button = event.target as ZnButton;
677
672
  if (button.disabled) return;
678
673
 
679
- (this.renderRoot.querySelectorAll('thead input[type="checkbox"]')[0] as HTMLInputElement).checked = false;
680
- for (const row of this.renderRoot.querySelectorAll('tbody input[type="checkbox"]')) {
681
- (row as HTMLInputElement).checked = false;
682
- }
683
-
684
674
  this.selectedRows = [];
685
675
  this.numberOfRowsSelected = 0;
676
+ this.updateKeys();
686
677
  this.requestUpdate();
687
678
  }
688
679
 
@@ -852,6 +843,10 @@ export default class ZnDataTable extends ZincElement {
852
843
  </td>`;
853
844
  }
854
845
 
846
+ private isRowSelected(row: Row): boolean {
847
+ return this.selectedRows.some((r: Row) => r.id === row.id);
848
+ }
849
+
855
850
  private getRows(data: Response): Row[] {
856
851
  const sourceRows = data.rows;
857
852