@kodaris/krubble-components 1.0.41 → 1.0.43

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.
@@ -296,7 +296,7 @@
296
296
  {
297
297
  "kind": "variable",
298
298
  "name": "KRTable",
299
- "default": "class KRTable extends i$2 { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._displayedColumns = []; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, label: col.label ?? '', sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); const content = typeof result === 'string' ? result : ''; if (content) { const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } el.innerHTML = content; this.appendChild(el); } }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (!this._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { this._def.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } break; default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._displayedColumns .map(id => this._def.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return b `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return b ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._def.title && !this._def.actions?.length && this._totalPages <= 1) { return A; } return b ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> ${this._def.dataSource?.mode === 'db' && !this._def.columns.some(col => col.searchable) ? b `<div class=\"search\"></div>` : b ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._def.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => b ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._def.actions?.length === 1 ? b ` <kr-button class=\"actions\" .href=${this._def.actions[0].href} .target=${this._def.actions[0].target} @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? b ` <kr-button class=\"actions\" .options=${this._def.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : A} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return b `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return b `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return b `<div class=\"status\">No data available</div>`; } return A; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return b ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getHeaderCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} >${col.label ?? col.id}${col.resizable !== false ? b `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : A}</div> `)} </div> ${this._data.map((row, rowIndex) => b ` <div class=\"row\"> ${this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `)} </div> `)} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._def.columns.length) { return b `<slot></slot>`; } return b ` ${this._renderHeader()} ${this._renderTable()} `; } }"
299
+ "default": "class KRTable extends i$2 { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._displayedColumns = []; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, label: col.label ?? '', sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); const content = typeof result === 'string' ? result : ''; if (content) { const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } el.innerHTML = content; this.appendChild(el); } }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (!this._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { this._def.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } break; default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // Clear any existing text selection on mousedown so we only detect // selections made during this click gesture, not stale selections from elsewhere _handleRowMouseDown() { if (!this._def.rowClickable) { return; } window.getSelection()?.removeAllRanges(); } _handleRowClick(row, rowIndex) { if (!this._def.rowClickable) { return; } const selection = window.getSelection(); if (selection && selection.toString().length > 0) { return; } this.dispatchEvent(new CustomEvent('row-click', { detail: { row, rowIndex }, bubbles: true, composed: true })); } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._displayedColumns .map(id => this._def.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return b `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return b ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._def.title && !this._def.actions?.length && this._totalPages <= 1) { return A; } return b ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> ${this._def.dataSource?.mode === 'db' && !this._def.columns.some(col => col.searchable) ? b `<div class=\"search\"></div>` : b ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._def.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => b ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._def.actions?.length === 1 ? b ` <kr-button class=\"actions\" .href=${this._def.actions[0].href} .target=${this._def.actions[0].target} @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? b ` <kr-button class=\"actions\" .options=${this._def.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : A} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return b `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return b `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return b `<div class=\"status\">No data available</div>`; } return A; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return b ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getHeaderCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} >${col.label ?? col.id}${col.resizable !== false ? b `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : A}</div> `)} </div> ${this._data.map((row, rowIndex) => b ` <div class=${e$1({ 'row': true, 'row--clickable': !!this._def.rowClickable })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} > ${this.getDisplayedColumns().map((col, i) => b ` <div class=${e$1(this._getCellClasses(col, i))} style=${o$1(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `)} </div> `)} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._def.columns.length) { return b `<slot></slot>`; } return b ` ${this._renderHeader()} ${this._renderTable()} `; } }"
300
300
  },
301
301
  {
302
302
  "kind": "variable",
@@ -626,7 +626,7 @@
626
626
  {
627
627
  "kind": "variable",
628
628
  "name": "Je",
629
- "default": "class extends re{constructor(){super(...arguments),this._scrollStyle=\"overlay\",this._data=[],this._dataState=\"idle\",this._page=1,this._pageSize=50,this._totalItems=0,this._totalPages=0,this._searchQuery=\"\",this._canScrollLeft=!1,this._canScrollRight=!1,this._canScrollHorizontal=!1,this._columnPickerOpen=!1,this._displayedColumns=[],this._resizing=null,this._resizeObserver=null,this._searchPositionLocked=!1,this._def={columns:[]},this.def={columns:[]},this._handleClickOutsideColumnPicker=e=>{if(!this._columnPickerOpen)return;const t=e.composedPath(),i=this.shadowRoot?.querySelector(\".column-picker-wrapper\");i&&!t.includes(i)&&(this._columnPickerOpen=!1)},this._handleResizeMove=e=>{if(!this._resizing)return;const t=this._def.columns.find((e=>e.id===this._resizing.columnId));if(t){const i=this._resizing.startWidth+(e.clientX-this._resizing.startX);t.width=`${Math.min(900,Math.max(50,i))}px`,this.requestUpdate()}},this._handleResizeEnd=()=>{this._resizing=null,document.removeEventListener(\"mousemove\",this._handleResizeMove),document.removeEventListener(\"mouseup\",this._handleResizeEnd)}}connectedCallback(){super.connectedCallback(),this.classList.toggle(\"kr-table--scroll-overlay\",\"overlay\"===this._scrollStyle),this.classList.toggle(\"kr-table--scroll-edge\",\"edge\"===this._scrollStyle),this._fetch(),this._initRefresh(),document.addEventListener(\"click\",this._handleClickOutsideColumnPicker),this._resizeObserver=new ResizeObserver((()=>{this._searchPositionLocked=!1,this._updateSearchPosition()})),this._resizeObserver.observe(this)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshTimer),document.removeEventListener(\"click\",this._handleClickOutsideColumnPicker),this._resizeObserver?.disconnect()}willUpdate(e){e.has(\"def\")&&(this._def={...this.def,columns:this.def.columns.map((e=>\"actions\"===e.type?{...e,label:e.label??\"\",sticky:\"right\",resizable:!1}:{...e}))},this._displayedColumns=this._def.displayedColumns||this._def.columns.map((e=>e.id)),this._fetch(),this._initRefresh())}updated(e){this._updateScrollFlags(),this._syncSlottedContent()}_syncSlottedContent(){const e=this.getDisplayedColumns().filter((e=>e.render));e.length&&(this.querySelectorAll('[slot^=\"cell-\"]').forEach((e=>e.remove())),this._data.forEach(((t,i)=>{e.forEach((e=>{const o=e.render(t),s=\"string\"==typeof o?o:\"\";if(s){const t=document.createElement(\"span\");t.slot=`cell-${i}-${e.id}`,\"actions\"===e.type&&(t.style.display=\"flex\",t.style.gap=\"8px\"),t.innerHTML=s,this.appendChild(t)}}))})))}refresh(){this._fetch()}goToPrevPage(){this._page>1&&(this._page--,this._fetch())}goToNextPage(){this._page<this._totalPages&&(this._page++,this._fetch())}goToPage(e){e>=1&&e<=this._totalPages&&(this._page=e,this._fetch())}_fetch(){if(!this._def.dataSource)return;let e;switch(this._dataState=\"loading\",this._def.dataSource.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]},this._searchQuery?.trim().length&&this._def.columns.filter((e=>e.searchable)).forEach((t=>{e.queryFields.push({name:t.id,operation:\"CONTAINS\",value:this._searchQuery,and:!1})}));break;default:e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]},this._searchQuery?.trim().length&&e.queryFields.push({name:\"_text_\",operation:\"IS\",value:Ge(this._searchQuery)})}this._def.dataSource.fetch(e).then((e=>{switch(this._def.dataSource?.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size;break}default:{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size}}this._dataState=\"success\",this._updateSearchPosition()})).catch((e=>{this._dataState=\"error\",Ne.show({message:e instanceof Error?e.message:\"Failed to load data\",type:\"error\"})}))}_initRefresh(){clearInterval(this._refreshTimer),this._def.refreshInterval&&this._def.refreshInterval>0&&(this._refreshTimer=window.setInterval((()=>{this._fetch()}),this._def.refreshInterval))}_handleSearch(e){const t=e.target;this._searchQuery=t.value,this._page=1,this._fetch()}_getGridTemplateColumns(){return this.getDisplayedColumns().map((e=>e.width?e.width:\"actions\"===e.type?\"max-content\":\"minmax(80px, auto)\")).join(\" \")}_updateSearchPosition(){if(this._searchPositionLocked)return;const e=this.shadowRoot?.querySelector(\".search\"),t=e?.querySelector(\".search-field\");e&&t&&(e.style.justifyContent=\"center\",t.style.marginLeft=\"\",requestAnimationFrame((()=>{const i=e.getBoundingClientRect(),o=t.getBoundingClientRect().left-i.left;e.style.justifyContent=\"flex-start\",t.style.marginLeft=`${o}px`,this._searchPositionLocked=!0})))}_toggleColumnPicker(){this._columnPickerOpen=!this._columnPickerOpen}_toggleColumn(e){this._displayedColumns.includes(e)?this._displayedColumns=this._displayedColumns.filter((t=>t!==e)):this._displayedColumns=[...this._displayedColumns,e]}getDisplayedColumns(){return this._displayedColumns.map((e=>this._def.columns.find((t=>t.id===e)))).sort(((e,t)=>\"actions\"===e.type&&\"actions\"!==t.type?1:\"actions\"!==e.type&&\"actions\"===t.type?-1:0))}_handleScroll(e){const t=e.target;this._canScrollLeft=t.scrollLeft>0,this._canScrollRight=t.scrollLeft<t.scrollWidth-t.clientWidth-1}_updateScrollFlags(){const e=this.shadowRoot?.querySelector(\".content\");e&&(this._canScrollLeft=e.scrollLeft>0,this._canScrollRight=e.scrollWidth>e.clientWidth&&e.scrollLeft<e.scrollWidth-e.clientWidth-1,this._canScrollHorizontal=e.scrollWidth>e.clientWidth),this.classList.toggle(\"kr-table--scroll-left-available\",this._canScrollLeft),this.classList.toggle(\"kr-table--scroll-right-available\",this._canScrollRight),this.classList.toggle(\"kr-table--scroll-horizontal-available\",this._canScrollHorizontal),this.classList.toggle(\"kr-table--sticky-left\",this.getDisplayedColumns().some((e=>\"left\"===e.sticky))),this.classList.toggle(\"kr-table--sticky-right\",this.getDisplayedColumns().some((e=>\"right\"===e.sticky)))}_handleResizeStart(e,t){e.preventDefault();const i=this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${t}\"]`);this._resizing={columnId:t,startX:e.clientX,startWidth:i?.offsetWidth||200},document.addEventListener(\"mousemove\",this._handleResizeMove),document.addEventListener(\"mouseup\",this._handleResizeEnd)}_handleAction(e){e.href||this.dispatchEvent(new CustomEvent(\"action\",{detail:{action:e.id},bubbles:!0,composed:!0}))}_renderCellContent(e,t,i){const o=t[e.id];if(e.render)return H`<slot name=\"cell-${i}-${e.id}\"></slot>`;if(null==o)return\"\";switch(e.type){case\"number\":return\"number\"==typeof o?o.toLocaleString():String(o);case\"currency\":return\"number\"==typeof o?o.toLocaleString(\"en-US\",{style:\"currency\",currency:\"USD\"}):String(o);case\"date\":{let e;if(o instanceof Date)e=o;else if(\"string\"==typeof o&&/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(o)){const t=o.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/,\"$1.$2\").replace(\" \",\"T\")+\"Z\";e=new Date(t)}else e=new Date(o);return e.toLocaleString(void 0,{year:\"numeric\",month:\"short\",day:\"numeric\",hour:\"numeric\",minute:\"2-digit\"})}case\"boolean\":return!0===o?\"Yes\":!1===o?\"No\":\"\";default:return String(o)}}_getHeaderCellClasses(e,t){return{\"header-cell\":!0,\"header-cell--align-center\":\"center\"===e.align,\"header-cell--align-right\":\"right\"===e.align,\"header-cell--sticky-left\":\"left\"===e.sticky,\"header-cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"header-cell--sticky-right\":\"right\"===e.sticky,\"header-cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellClasses(e,t){return{cell:!0,\"cell--actions\":\"actions\"===e.type,\"cell--align-center\":\"center\"===e.align,\"cell--align-right\":\"right\"===e.align,\"cell--sticky-left\":\"left\"===e.sticky,\"cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"cell--sticky-right\":\"right\"===e.sticky,\"cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellStyle(e,t){const i={};if(\"left\"===e.sticky){let e=0;for(let i=0;i<t;i++){const t=this.getDisplayedColumns()[i];\"left\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.left=`${e}px`}if(\"right\"===e.sticky){let e=0;for(let i=t+1;i<this.getDisplayedColumns().length;i++){const t=this.getDisplayedColumns()[i];\"right\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.right=`${e}px`}return i}_renderPagination(){const e=(this._page-1)*this._pageSize+1,t=Math.min(this._page*this._pageSize,this._totalItems);return H` <div class=\"pagination\"> <span class=\"pagination-icon ${1===this._page?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${e}-${t} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page===this._totalPages?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `}_renderHeader(){return!this._def.title&&!this._def.actions?.length&&this._totalPages<=1?U:H` <div class=\"header\"> <div class=\"title\">${this._def.title??\"\"}</div> ${\"db\"!==this._def.dataSource?.mode||this._def.columns.some((e=>e.searchable))?H` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `:H`<div class=\"search\"></div>`} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${()=>this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen?\"open\":\"\"}\"> ${[...this._def.columns].filter((e=>\"actions\"!==e.type)).sort(((e,t)=>(e.label??e.id).localeCompare(t.label??t.id))).map((e=>H` <div class=\"column-picker-item\" @click=${()=>this._toggleColumn(e.id)}> <div class=\"column-picker-checkbox ${this._displayedColumns.includes(e.id)?\"checked\":\"\"}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${e.label??e.id}</span> </div> `))} </div> </div> ${1===this._def.actions?.length?H` <kr-button class=\"actions\" .href=${this._def.actions[0].href} .target=${this._def.actions[0].target} @click=${()=>this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> `:this._def.actions?.length?H` <kr-button class=\"actions\" .options=${this._def.actions.map((e=>({id:e.id,label:e.label})))} @option-select=${e=>this._handleAction({id:e.detail.id,label:e.detail.label})} > Actions </kr-button> `:U} </div> </div> `}_renderStatus(){return\"loading\"===this._dataState&&0===this._data.length?H`<div class=\"status\">Loading...</div>`:\"error\"===this._dataState&&0===this._data.length?H`<div class=\"status status--error\">Error loading data</div>`:0===this._data.length?H`<div class=\"status\">No data available</div>`:U}_renderTable(){return H` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map(((e,t)=>H` <div class=${ke(this._getHeaderCellClasses(e,t))} style=${je(this._getCellStyle(e,t))} data-column-id=${e.id} >${e.label??e.id}${!1!==e.resizable?H`<div class=\"header-cell__resize\" @mousedown=${t=>this._handleResizeStart(t,e.id)} ></div>`:U}</div> `))} </div> ${this._data.map(((e,t)=>H` <div class=\"row\"> ${this.getDisplayedColumns().map(((i,o)=>H` <div class=${ke(this._getCellClasses(i,o))} style=${je(this._getCellStyle(i,o))} data-column-id=${i.id} > ${this._renderCellContent(i,e,t)} </div> `))} </div> `))} </div> </div> </div> `}render(){return this._def.columns.length?H` ${this._renderHeader()} ${this._renderTable()} `:H`<slot></slot>`}}"
629
+ "default": "class extends re{constructor(){super(...arguments),this._scrollStyle=\"overlay\",this._data=[],this._dataState=\"idle\",this._page=1,this._pageSize=50,this._totalItems=0,this._totalPages=0,this._searchQuery=\"\",this._canScrollLeft=!1,this._canScrollRight=!1,this._canScrollHorizontal=!1,this._columnPickerOpen=!1,this._displayedColumns=[],this._resizing=null,this._resizeObserver=null,this._searchPositionLocked=!1,this._def={columns:[]},this.def={columns:[]},this._handleClickOutsideColumnPicker=e=>{if(!this._columnPickerOpen)return;const t=e.composedPath(),i=this.shadowRoot?.querySelector(\".column-picker-wrapper\");i&&!t.includes(i)&&(this._columnPickerOpen=!1)},this._handleResizeMove=e=>{if(!this._resizing)return;const t=this._def.columns.find((e=>e.id===this._resizing.columnId));if(t){const i=this._resizing.startWidth+(e.clientX-this._resizing.startX);t.width=`${Math.min(900,Math.max(50,i))}px`,this.requestUpdate()}},this._handleResizeEnd=()=>{this._resizing=null,document.removeEventListener(\"mousemove\",this._handleResizeMove),document.removeEventListener(\"mouseup\",this._handleResizeEnd)}}connectedCallback(){super.connectedCallback(),this.classList.toggle(\"kr-table--scroll-overlay\",\"overlay\"===this._scrollStyle),this.classList.toggle(\"kr-table--scroll-edge\",\"edge\"===this._scrollStyle),this._fetch(),this._initRefresh(),document.addEventListener(\"click\",this._handleClickOutsideColumnPicker),this._resizeObserver=new ResizeObserver((()=>{this._searchPositionLocked=!1,this._updateSearchPosition()})),this._resizeObserver.observe(this)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshTimer),document.removeEventListener(\"click\",this._handleClickOutsideColumnPicker),this._resizeObserver?.disconnect()}willUpdate(e){e.has(\"def\")&&(this._def={...this.def,columns:this.def.columns.map((e=>\"actions\"===e.type?{...e,label:e.label??\"\",sticky:\"right\",resizable:!1}:{...e}))},this._displayedColumns=this._def.displayedColumns||this._def.columns.map((e=>e.id)),this._fetch(),this._initRefresh())}updated(e){this._updateScrollFlags(),this._syncSlottedContent()}_syncSlottedContent(){const e=this.getDisplayedColumns().filter((e=>e.render));e.length&&(this.querySelectorAll('[slot^=\"cell-\"]').forEach((e=>e.remove())),this._data.forEach(((t,i)=>{e.forEach((e=>{const o=e.render(t),s=\"string\"==typeof o?o:\"\";if(s){const t=document.createElement(\"span\");t.slot=`cell-${i}-${e.id}`,\"actions\"===e.type&&(t.style.display=\"flex\",t.style.gap=\"8px\"),t.innerHTML=s,this.appendChild(t)}}))})))}refresh(){this._fetch()}goToPrevPage(){this._page>1&&(this._page--,this._fetch())}goToNextPage(){this._page<this._totalPages&&(this._page++,this._fetch())}goToPage(e){e>=1&&e<=this._totalPages&&(this._page=e,this._fetch())}_fetch(){if(!this._def.dataSource)return;let e;switch(this._dataState=\"loading\",this._def.dataSource.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]},this._searchQuery?.trim().length&&this._def.columns.filter((e=>e.searchable)).forEach((t=>{e.queryFields.push({name:t.id,operation:\"CONTAINS\",value:this._searchQuery,and:!1})}));break;default:e={page:this._page-1,size:this._pageSize,sorts:[],filterFields:[],queryFields:[],facetFields:[]},this._searchQuery?.trim().length&&e.queryFields.push({name:\"_text_\",operation:\"IS\",value:Ge(this._searchQuery)})}this._def.dataSource.fetch(e).then((e=>{switch(this._def.dataSource?.mode){case\"opensearch\":throw Error(\"Opensearch not supported yet\");case\"db\":{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size;break}default:{const t=e;this._data=t.data.content,this._totalItems=t.data.totalElements,this._totalPages=t.data.totalPages,this._pageSize=t.data.size}}this._dataState=\"success\",this._updateSearchPosition()})).catch((e=>{this._dataState=\"error\",Ne.show({message:e instanceof Error?e.message:\"Failed to load data\",type:\"error\"})}))}_initRefresh(){clearInterval(this._refreshTimer),this._def.refreshInterval&&this._def.refreshInterval>0&&(this._refreshTimer=window.setInterval((()=>{this._fetch()}),this._def.refreshInterval))}_handleSearch(e){const t=e.target;this._searchQuery=t.value,this._page=1,this._fetch()}_getGridTemplateColumns(){return this.getDisplayedColumns().map((e=>e.width?e.width:\"actions\"===e.type?\"max-content\":\"minmax(80px, auto)\")).join(\" \")}_updateSearchPosition(){if(this._searchPositionLocked)return;const e=this.shadowRoot?.querySelector(\".search\"),t=e?.querySelector(\".search-field\");e&&t&&(e.style.justifyContent=\"center\",t.style.marginLeft=\"\",requestAnimationFrame((()=>{const i=e.getBoundingClientRect(),o=t.getBoundingClientRect().left-i.left;e.style.justifyContent=\"flex-start\",t.style.marginLeft=`${o}px`,this._searchPositionLocked=!0})))}_toggleColumnPicker(){this._columnPickerOpen=!this._columnPickerOpen}_toggleColumn(e){this._displayedColumns.includes(e)?this._displayedColumns=this._displayedColumns.filter((t=>t!==e)):this._displayedColumns=[...this._displayedColumns,e]}_handleRowMouseDown(){this._def.rowClickable&&window.getSelection()?.removeAllRanges()}_handleRowClick(e,t){if(!this._def.rowClickable)return;const i=window.getSelection();i&&i.toString().length>0||this.dispatchEvent(new CustomEvent(\"row-click\",{detail:{row:e,rowIndex:t},bubbles:!0,composed:!0}))}getDisplayedColumns(){return this._displayedColumns.map((e=>this._def.columns.find((t=>t.id===e)))).sort(((e,t)=>\"actions\"===e.type&&\"actions\"!==t.type?1:\"actions\"!==e.type&&\"actions\"===t.type?-1:0))}_handleScroll(e){const t=e.target;this._canScrollLeft=t.scrollLeft>0,this._canScrollRight=t.scrollLeft<t.scrollWidth-t.clientWidth-1}_updateScrollFlags(){const e=this.shadowRoot?.querySelector(\".content\");e&&(this._canScrollLeft=e.scrollLeft>0,this._canScrollRight=e.scrollWidth>e.clientWidth&&e.scrollLeft<e.scrollWidth-e.clientWidth-1,this._canScrollHorizontal=e.scrollWidth>e.clientWidth),this.classList.toggle(\"kr-table--scroll-left-available\",this._canScrollLeft),this.classList.toggle(\"kr-table--scroll-right-available\",this._canScrollRight),this.classList.toggle(\"kr-table--scroll-horizontal-available\",this._canScrollHorizontal),this.classList.toggle(\"kr-table--sticky-left\",this.getDisplayedColumns().some((e=>\"left\"===e.sticky))),this.classList.toggle(\"kr-table--sticky-right\",this.getDisplayedColumns().some((e=>\"right\"===e.sticky)))}_handleResizeStart(e,t){e.preventDefault();const i=this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${t}\"]`);this._resizing={columnId:t,startX:e.clientX,startWidth:i?.offsetWidth||200},document.addEventListener(\"mousemove\",this._handleResizeMove),document.addEventListener(\"mouseup\",this._handleResizeEnd)}_handleAction(e){e.href||this.dispatchEvent(new CustomEvent(\"action\",{detail:{action:e.id},bubbles:!0,composed:!0}))}_renderCellContent(e,t,i){const o=t[e.id];if(e.render)return H`<slot name=\"cell-${i}-${e.id}\"></slot>`;if(null==o)return\"\";switch(e.type){case\"number\":return\"number\"==typeof o?o.toLocaleString():String(o);case\"currency\":return\"number\"==typeof o?o.toLocaleString(\"en-US\",{style:\"currency\",currency:\"USD\"}):String(o);case\"date\":{let e;if(o instanceof Date)e=o;else if(\"string\"==typeof o&&/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(o)){const t=o.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/,\"$1.$2\").replace(\" \",\"T\")+\"Z\";e=new Date(t)}else e=new Date(o);return e.toLocaleString(void 0,{year:\"numeric\",month:\"short\",day:\"numeric\",hour:\"numeric\",minute:\"2-digit\"})}case\"boolean\":return!0===o?\"Yes\":!1===o?\"No\":\"\";default:return String(o)}}_getHeaderCellClasses(e,t){return{\"header-cell\":!0,\"header-cell--align-center\":\"center\"===e.align,\"header-cell--align-right\":\"right\"===e.align,\"header-cell--sticky-left\":\"left\"===e.sticky,\"header-cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"header-cell--sticky-right\":\"right\"===e.sticky,\"header-cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellClasses(e,t){return{cell:!0,\"cell--actions\":\"actions\"===e.type,\"cell--align-center\":\"center\"===e.align,\"cell--align-right\":\"right\"===e.align,\"cell--sticky-left\":\"left\"===e.sticky,\"cell--sticky-left-last\":\"left\"===e.sticky&&!this.getDisplayedColumns().slice(t+1).some((e=>\"left\"===e.sticky)),\"cell--sticky-right\":\"right\"===e.sticky,\"cell--sticky-right-first\":\"right\"===e.sticky&&!this.getDisplayedColumns().slice(0,t).some((e=>\"right\"===e.sticky))}}_getCellStyle(e,t){const i={};if(\"left\"===e.sticky){let e=0;for(let i=0;i<t;i++){const t=this.getDisplayedColumns()[i];\"left\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.left=`${e}px`}if(\"right\"===e.sticky){let e=0;for(let i=t+1;i<this.getDisplayedColumns().length;i++){const t=this.getDisplayedColumns()[i];\"right\"===t.sticky&&(e+=parseInt(t.width||\"0\",10))}i.right=`${e}px`}return i}_renderPagination(){const e=(this._page-1)*this._pageSize+1,t=Math.min(this._page*this._pageSize,this._totalItems);return H` <div class=\"pagination\"> <span class=\"pagination-icon ${1===this._page?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${e}-${t} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page===this._totalPages?\"pagination-icon--disabled\":\"\"}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `}_renderHeader(){return!this._def.title&&!this._def.actions?.length&&this._totalPages<=1?U:H` <div class=\"header\"> <div class=\"title\">${this._def.title??\"\"}</div> ${\"db\"!==this._def.dataSource?.mode||this._def.columns.some((e=>e.searchable))?H` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `:H`<div class=\"search\"></div>`} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${()=>this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen?\"open\":\"\"}\"> ${[...this._def.columns].filter((e=>\"actions\"!==e.type)).sort(((e,t)=>(e.label??e.id).localeCompare(t.label??t.id))).map((e=>H` <div class=\"column-picker-item\" @click=${()=>this._toggleColumn(e.id)}> <div class=\"column-picker-checkbox ${this._displayedColumns.includes(e.id)?\"checked\":\"\"}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${e.label??e.id}</span> </div> `))} </div> </div> ${1===this._def.actions?.length?H` <kr-button class=\"actions\" .href=${this._def.actions[0].href} .target=${this._def.actions[0].target} @click=${()=>this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> `:this._def.actions?.length?H` <kr-button class=\"actions\" .options=${this._def.actions.map((e=>({id:e.id,label:e.label})))} @option-select=${e=>this._handleAction({id:e.detail.id,label:e.detail.label})} > Actions </kr-button> `:U} </div> </div> `}_renderStatus(){return\"loading\"===this._dataState&&0===this._data.length?H`<div class=\"status\">Loading...</div>`:\"error\"===this._dataState&&0===this._data.length?H`<div class=\"status status--error\">Error loading data</div>`:0===this._data.length?H`<div class=\"status\">No data available</div>`:U}_renderTable(){return H` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map(((e,t)=>H` <div class=${ke(this._getHeaderCellClasses(e,t))} style=${je(this._getCellStyle(e,t))} data-column-id=${e.id} >${e.label??e.id}${!1!==e.resizable?H`<div class=\"header-cell__resize\" @mousedown=${t=>this._handleResizeStart(t,e.id)} ></div>`:U}</div> `))} </div> ${this._data.map(((e,t)=>H` <div class=${ke({row:!0,\"row--clickable\":!!this._def.rowClickable})} @mousedown=${()=>this._handleRowMouseDown()} @click=${()=>this._handleRowClick(e,t)} > ${this.getDisplayedColumns().map(((i,o)=>H` <div class=${ke(this._getCellClasses(i,o))} style=${je(this._getCellStyle(i,o))} data-column-id=${i.id} > ${this._renderCellContent(i,e,t)} </div> `))} </div> `))} </div> </div> </div> `}render(){return this._def.columns.length?H` ${this._renderHeader()} ${this._renderTable()} `:H`<slot></slot>`}}"
630
630
  },
631
631
  {
632
632
  "kind": "variable",
@@ -1590,7 +1590,7 @@
1590
1590
  {
1591
1591
  "kind": "variable",
1592
1592
  "name": "KRTable",
1593
- "default": "class KRTable extends LitElement { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._displayedColumns = []; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, label: col.label ?? '', sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); const content = typeof result === 'string' ? result : ''; if (content) { const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } el.innerHTML = content; this.appendChild(el); } }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (!this._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { this._def.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } break; default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); break; } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._displayedColumns .map(id => this._def.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return html `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return html ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._def.title && !this._def.actions?.length && this._totalPages <= 1) { return nothing; } return html ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> ${this._def.dataSource?.mode === 'db' && !this._def.columns.some(col => col.searchable) ? html `<div class=\"search\"></div>` : html ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._def.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => html ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._def.actions?.length === 1 ? html ` <kr-button class=\"actions\" .href=${this._def.actions[0].href} .target=${this._def.actions[0].target} @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? html ` <kr-button class=\"actions\" .options=${this._def.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : nothing} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return html `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return html `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return html `<div class=\"status\">No data available</div>`; } return nothing; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return html ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getHeaderCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} >${col.label ?? col.id}${col.resizable !== false ? html `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : nothing}</div> `)} </div> ${this._data.map((row, rowIndex) => html ` <div class=\"row\"> ${this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `)} </div> `)} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._def.columns.length) { return html `<slot></slot>`; } return html ` ${this._renderHeader()} ${this._renderTable()} `; } }"
1593
+ "default": "class KRTable extends LitElement { constructor() { super(...arguments); /** * Internal flag to switch between scroll edge modes: * - 'overlay': Fixed padding with overlay elements that hide content at edges (scrollbar at viewport edge) * - 'edge': Padding scrolls with content, allowing table to reach edges when scrolling */ this._scrollStyle = 'overlay'; this._data = []; this._dataState = 'idle'; this._page = 1; this._pageSize = 50; this._totalItems = 0; this._totalPages = 0; this._searchQuery = ''; this._canScrollLeft = false; this._canScrollRight = false; this._canScrollHorizontal = false; this._columnPickerOpen = false; this._displayedColumns = []; this._resizing = null; this._resizeObserver = null; this._searchPositionLocked = false; this._def = { columns: [] }; this.def = { columns: [] }; this._handleClickOutsideColumnPicker = (e) => { if (!this._columnPickerOpen) return; const path = e.composedPath(); const picker = this.shadowRoot?.querySelector('.column-picker-wrapper'); if (picker && !path.includes(picker)) { this._columnPickerOpen = false; } }; this._handleResizeMove = (e) => { if (!this._resizing) return; const col = this._def.columns.find(c => c.id === this._resizing.columnId); if (col) { const newWidth = this._resizing.startWidth + (e.clientX - this._resizing.startX); col.width = `${Math.min(900, Math.max(50, newWidth))}px`; this.requestUpdate(); } }; this._handleResizeEnd = () => { this._resizing = null; document.removeEventListener('mousemove', this._handleResizeMove); document.removeEventListener('mouseup', this._handleResizeEnd); }; } connectedCallback() { super.connectedCallback(); this.classList.toggle('kr-table--scroll-overlay', this._scrollStyle === 'overlay'); this.classList.toggle('kr-table--scroll-edge', this._scrollStyle === 'edge'); this._fetch(); this._initRefresh(); document.addEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver = new ResizeObserver(() => { // Unlock and recalculate on resize since layout changes this._searchPositionLocked = false; this._updateSearchPosition(); }); this._resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshTimer); document.removeEventListener('click', this._handleClickOutsideColumnPicker); this._resizeObserver?.disconnect(); } willUpdate(changedProperties) { if (changedProperties.has('def')) { // Copy user's def and normalize action columns this._def = { ...this.def, columns: this.def.columns.map(col => { if (col.type === 'actions') { return { ...col, label: col.label ?? '', sticky: 'right', resizable: false }; } return { ...col }; }) }; this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id); this._fetch(); this._initRefresh(); } } updated(changedProperties) { this._updateScrollFlags(); this._syncSlottedContent(); } /** Syncs light DOM content for cells with custom render functions */ _syncSlottedContent() { const columns = this.getDisplayedColumns().filter(col => col.render); if (!columns.length) return; // Clear old slotted content this.querySelectorAll('[slot^=\"cell-\"]').forEach(el => el.remove()); // Create new slotted content this._data.forEach((row, rowIndex) => { columns.forEach(col => { const result = col.render(row); const content = typeof result === 'string' ? result : ''; if (content) { const el = document.createElement('span'); el.slot = `cell-${rowIndex}-${col.id}`; if (col.type === 'actions') { el.style.display = 'flex'; el.style.gap = '8px'; } el.innerHTML = content; this.appendChild(el); } }); }); } // ---------------------------------------------------------------------------- // Public Interface // ---------------------------------------------------------------------------- refresh() { this._fetch(); } goToPrevPage() { if (this._page > 1) { this._page--; this._fetch(); } } goToNextPage() { if (this._page < this._totalPages) { this._page++; this._fetch(); } } goToPage(page) { if (page >= 1 && page <= this._totalPages) { this._page = page; this._fetch(); } } // ---------------------------------------------------------------------------- // Data Fetching // ---------------------------------------------------------------------------- /** * Fetches data from the API and updates the table. * Shows a loading spinner while fetching, then displays rows on success * or an error snackbar on failure. * Request/response format depends on dataSource.mode (solr, opensearch, db). */ _fetch() { if (!this._def.dataSource) return; this._dataState = 'loading'; // Build request based on mode let request; switch (this._def.dataSource.mode) { case 'opensearch': throw Error('Opensearch not supported yet'); case 'db': request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { this._def.columns.filter(col => col.searchable).forEach(col => { request.queryFields.push({ name: col.id, operation: 'CONTAINS', value: this._searchQuery, and: false }); }); } break; default: // solr request = { page: this._page - 1, size: this._pageSize, sorts: [], filterFields: [], queryFields: [], facetFields: [] }; if (this._searchQuery?.trim().length) { request.queryFields.push({ name: '_text_', operation: 'IS', value: escapeSolrQuery(this._searchQuery) }); } } this._def.dataSource.fetch(request) .then(response => { // Parse response based on mode switch (this._def.dataSource?.mode) { case 'opensearch': { throw Error('Opensearch not supported yet'); break; } case 'db': { const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; break; } default: { // solr const res = response; this._data = res.data.content; this._totalItems = res.data.totalElements; this._totalPages = res.data.totalPages; this._pageSize = res.data.size; } } this._dataState = 'success'; this._updateSearchPosition(); }) .catch(err => { this._dataState = 'error'; KRSnackbar.show({ message: err instanceof Error ? err.message : 'Failed to load data', type: 'error' }); }); } /** * Sets up auto-refresh so the table automatically fetches fresh data * at a regular interval (useful for dashboards, monitoring views). * Configured via def.refreshInterval in milliseconds. */ _initRefresh() { clearInterval(this._refreshTimer); if (this._def.refreshInterval && this._def.refreshInterval > 0) { this._refreshTimer = window.setInterval(() => { this._fetch(); }, this._def.refreshInterval); } } _handleSearch(e) { const input = e.target; this._searchQuery = input.value; this._page = 1; this._fetch(); } _getGridTemplateColumns() { const cols = this.getDisplayedColumns(); return cols.map((col) => { // If column has explicit width, use it if (col.width) { return col.width; } // Actions columns: fit content without minimum if (col.type === 'actions') { return 'max-content'; } // No width specified - use content-based sizing with minimum return 'minmax(80px, auto)'; }).join(' '); } /** * Updates search position to be centered with equal gaps from title and tools. * On first call: resets to flex centering, measures position, then locks with fixed margin. * Subsequent calls are ignored unless _searchPositionLocked is reset (e.g., on resize). */ _updateSearchPosition() { // Skip if already locked (prevents shifts on pagination changes) if (this._searchPositionLocked) return; const search = this.shadowRoot?.querySelector('.search'); const searchField = search?.querySelector('.search-field'); if (!search || !searchField) return; // Reset to flex centering search.style.justifyContent = 'center'; searchField.style.marginLeft = ''; requestAnimationFrame(() => { const searchRect = search.getBoundingClientRect(); const fieldRect = searchField.getBoundingClientRect(); // Calculate how far from the left of search container the field currently is const currentOffset = fieldRect.left - searchRect.left; // Lock position: switch to flex-start and use fixed margin search.style.justifyContent = 'flex-start'; searchField.style.marginLeft = `${currentOffset}px`; // Mark as locked so pagination changes don't shift the search this._searchPositionLocked = true; }); } // ---------------------------------------------------------------------------- // Columns // ---------------------------------------------------------------------------- _toggleColumnPicker() { this._columnPickerOpen = !this._columnPickerOpen; } _toggleColumn(columnId) { if (this._displayedColumns.includes(columnId)) { this._displayedColumns = this._displayedColumns.filter(id => id !== columnId); } else { this._displayedColumns = [...this._displayedColumns, columnId]; } } // Clear any existing text selection on mousedown so we only detect // selections made during this click gesture, not stale selections from elsewhere _handleRowMouseDown() { if (!this._def.rowClickable) { return; } window.getSelection()?.removeAllRanges(); } _handleRowClick(row, rowIndex) { if (!this._def.rowClickable) { return; } const selection = window.getSelection(); if (selection && selection.toString().length > 0) { return; } this.dispatchEvent(new CustomEvent('row-click', { detail: { row, rowIndex }, bubbles: true, composed: true })); } // When a user toggles a column on via the column picker, it gets appended // to _displayedColumns. By mapping over _displayedColumns (not def.columns), // the new column appears at the right edge of the table instead of jumping // back to its original position in the column definition. // Actions columns are always moved to the end. getDisplayedColumns() { return this._displayedColumns .map(id => this._def.columns.find(col => col.id === id)) .sort((a, b) => { if (a.type === 'actions' && b.type !== 'actions') return 1; if (a.type !== 'actions' && b.type === 'actions') return -1; return 0; }); } // ---------------------------------------------------------------------------- // Scrolling // ---------------------------------------------------------------------------- /** * Scroll event handler that updates scroll flags in real-time as user scrolls. * Updates shadow indicators to show if more content exists left/right. */ _handleScroll(e) { const container = e.target; this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollLeft < container.scrollWidth - container.clientWidth - 1; } /** * Updates scroll state flags for the table content container. * - _canScrollLeft: true if scrolled right (can scroll back left) * - _canScrollRight: true if more content exists to the right * - _canScrollHorizontal: true if content is wider than container * These flags control scroll shadow indicators and CSS classes. */ _updateScrollFlags() { const container = this.shadowRoot?.querySelector('.content'); if (container) { this._canScrollLeft = container.scrollLeft > 0; this._canScrollRight = container.scrollWidth > container.clientWidth && container.scrollLeft < container.scrollWidth - container.clientWidth - 1; this._canScrollHorizontal = container.scrollWidth > container.clientWidth; } this.classList.toggle('kr-table--scroll-left-available', this._canScrollLeft); this.classList.toggle('kr-table--scroll-right-available', this._canScrollRight); this.classList.toggle('kr-table--scroll-horizontal-available', this._canScrollHorizontal); this.classList.toggle('kr-table--sticky-left', this.getDisplayedColumns().some(c => c.sticky === 'left')); this.classList.toggle('kr-table--sticky-right', this.getDisplayedColumns().some(c => c.sticky === 'right')); } // ---------------------------------------------------------------------------- // Column Resizing // ---------------------------------------------------------------------------- _handleResizeStart(e, columnId) { e.preventDefault(); const headerCell = this.shadowRoot?.querySelector(`.header-cell[data-column-id=\"${columnId}\"]`); this._resizing = { columnId, startX: e.clientX, startWidth: headerCell?.offsetWidth || 200 }; document.addEventListener('mousemove', this._handleResizeMove); document.addEventListener('mouseup', this._handleResizeEnd); } // ---------------------------------------------------------------------------- // Header // ---------------------------------------------------------------------------- _handleAction(action) { if (action.href) { return; } this.dispatchEvent(new CustomEvent('action', { detail: { action: action.id }, bubbles: true, composed: true })); } // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- _renderCellContent(column, row, rowIndex) { const value = row[column.id]; if (column.render) { // Use slot to project content from light DOM so external styles apply return html `<slot name=\"cell-${rowIndex}-${column.id}\"></slot>`; } if (value === null || value === undefined) { return ''; } switch (column.type) { case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value); case 'currency': return typeof value === 'number' ? value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) : String(value); case 'date': { let date; if (value instanceof Date) { date = value; } else if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/.test(value)) { // MySQL datetime format (UTC): \"2026-01-28 01:33:44:517\" // Replace last colon before ms with dot, append Z for UTC const isoString = value.replace(/(\\d{2}:\\d{2}:\\d{2}):(\\d+)$/, '$1.$2').replace(' ', 'T') + 'Z'; date = new Date(isoString); } else { date = new Date(value); } // Show date and time for datetime values return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } case 'boolean': if (value === true) return 'Yes'; if (value === false) return 'No'; return ''; default: return String(value); } } /** * Returns CSS classes for a header cell based on column config. */ _getHeaderCellClasses(column, index) { return { 'header-cell': true, 'header-cell--align-center': column.align === 'center', 'header-cell--align-right': column.align === 'right', 'header-cell--sticky-left': column.sticky === 'left', 'header-cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'header-cell--sticky-right': column.sticky === 'right', 'header-cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns CSS classes for a table cell based on column config: * - Alignment (center, right) * - Sticky positioning (left, right) * - Border classes for the last left-sticky or first right-sticky column */ _getCellClasses(column, index) { return { 'cell': true, 'cell--actions': column.type === 'actions', 'cell--align-center': column.align === 'center', 'cell--align-right': column.align === 'right', 'cell--sticky-left': column.sticky === 'left', 'cell--sticky-left-last': column.sticky === 'left' && !this.getDisplayedColumns().slice(index + 1).some(c => c.sticky === 'left'), 'cell--sticky-right': column.sticky === 'right', 'cell--sticky-right-first': column.sticky === 'right' && !this.getDisplayedColumns().slice(0, index).some(c => c.sticky === 'right') }; } /** * Returns inline styles for a table cell: * - Width (from column config or default 150px) * - Min-width (if specified) * - Left/right offset for sticky columns (calculated from widths of preceding sticky columns) */ _getCellStyle(column, index) { const styles = {}; if (column.sticky === 'left') { let leftOffset = 0; for (let i = 0; i < index; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'left') { leftOffset += parseInt(col.width || '0', 10); } } styles.left = `${leftOffset}px`; } if (column.sticky === 'right') { let rightOffset = 0; for (let i = index + 1; i < this.getDisplayedColumns().length; i++) { const col = this.getDisplayedColumns()[i]; if (col.sticky === 'right') { rightOffset += parseInt(col.width || '0', 10); } } styles.right = `${rightOffset}px`; } return styles; } /** * Renders the pagination controls: * - Previous page arrow (disabled on first page) * - Range text showing \"1-50 of 150\" format * - Next page arrow (disabled on last page) * * Hidden when there's no data or all data fits on one page. */ _renderPagination() { const start = (this._page - 1) * this._pageSize + 1; const end = Math.min(this._page * this._pageSize, this._totalItems); return html ` <div class=\"pagination\"> <span class=\"pagination-icon ${this._page === 1 ? 'pagination-icon--disabled' : ''}\" @click=${this.goToPrevPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\"/></svg> </span> <span class=\"pagination-info\">${start}-${end} of ${this._totalItems}</span> <span class=\"pagination-icon ${this._page === this._totalPages ? 'pagination-icon--disabled' : ''}\" @click=${this.goToNextPage} > <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg> </span> </div> `; } /** * Renders the header toolbar containing: * - Title (left) * - Search bar with view selector dropdown (center) * - Tools (right): page navigation, refresh button, column visibility picker, actions dropdown * * Hidden when there's no title, no actions, and data fits on one page. */ _renderHeader() { if (!this._def.title && !this._def.actions?.length && this._totalPages <= 1) { return nothing; } return html ` <div class=\"header\"> <div class=\"title\">${this._def.title ?? ''}</div> ${this._def.dataSource?.mode === 'db' && !this._def.columns.some(col => col.searchable) ? html `<div class=\"search\"></div>` : html ` <div class=\"search\"> <!-- TODO: Saved views dropdown <div class=\"views\"> <span>Default View</span> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z\"/></svg> </div> --> <div class=\"search-field\"> <svg class=\"search-icon\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\"/></svg> <input type=\"text\" class=\"search-input\" placeholder=\"Search...\" .value=${this._searchQuery} @input=${this._handleSearch} /> </div> </div> `} <div class=\"tools\"> ${this._renderPagination()} <span class=\"refresh\" title=\"Refresh\" @click=${() => this.refresh()}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z\"/></svg> </span> <div class=\"column-picker-wrapper\"> <span class=\"header-icon\" title=\"Columns\" @click=${this._toggleColumnPicker}> <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"M121-280v-400q0-33 23.5-56.5T201-760h559q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H201q-33 0-56.5-23.5T121-280Zm79 0h133v-400H200v400Zm213 0h133v-400H413v400Zm213 0h133v-400H626v400Z\"/></svg> </span> <div class=\"column-picker ${this._columnPickerOpen ? 'open' : ''}\"> ${[...this._def.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => html ` <div class=\"column-picker-item\" @click=${() => this._toggleColumn(col.id)}> <div class=\"column-picker-checkbox ${this._displayedColumns.includes(col.id) ? 'checked' : ''}\"> <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg> </div> <span class=\"column-picker-label\">${col.label ?? col.id}</span> </div> `)} </div> </div> ${this._def.actions?.length === 1 ? html ` <kr-button class=\"actions\" .href=${this._def.actions[0].href} .target=${this._def.actions[0].target} @click=${() => this._handleAction(this._def.actions[0])} > ${this._def.actions[0].label} </kr-button> ` : this._def.actions?.length ? html ` <kr-button class=\"actions\" .options=${this._def.actions.map(a => ({ id: a.id, label: a.label }))} @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })} > Actions </kr-button> ` : nothing} </div> </div> `; } /** Renders status message (loading, error, empty) */ _renderStatus() { if (this._dataState === 'loading' && this._data.length === 0) { return html `<div class=\"status\">Loading...</div>`; } if (this._dataState === 'error' && this._data.length === 0) { return html `<div class=\"status status--error\">Error loading data</div>`; } if (this._data.length === 0) { return html `<div class=\"status\">No data available</div>`; } return nothing; } /** Renders the scrollable data grid with column headers and rows. */ _renderTable() { return html ` <div class=\"wrapper\"> <div class=\"overlay-left\"></div> <div class=\"overlay-right\"></div> ${this._renderStatus()} <div class=\"content\" @scroll=${this._handleScroll}> <div class=\"table\" style=\"grid-template-columns: ${this._getGridTemplateColumns()}\"> <div class=\"header-row\"> ${this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getHeaderCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} >${col.label ?? col.id}${col.resizable !== false ? html `<div class=\"header-cell__resize\" @mousedown=${(e) => this._handleResizeStart(e, col.id)} ></div>` : nothing}</div> `)} </div> ${this._data.map((row, rowIndex) => html ` <div class=${classMap({ 'row': true, 'row--clickable': !!this._def.rowClickable })} @mousedown=${() => this._handleRowMouseDown()} @click=${() => this._handleRowClick(row, rowIndex)} > ${this.getDisplayedColumns().map((col, i) => html ` <div class=${classMap(this._getCellClasses(col, i))} style=${styleMap(this._getCellStyle(col, i))} data-column-id=${col.id} > ${this._renderCellContent(col, row, rowIndex)} </div> `)} </div> `)} </div> </div> </div> `; } /** * Renders a data table with: * - Header bar with title, search input with view selector, and tools (pagination, refresh, column visibility, actions dropdown) * - Scrollable grid with sticky header row and optional sticky left/right columns * - Loading, error message, or empty state when no data */ render() { if (!this._def.columns.length) { return html `<slot></slot>`; } return html ` ${this._renderHeader()} ${this._renderTable()} `; } }"
1594
1594
  }
1595
1595
  ],
1596
1596
  "exports": [
@@ -3587,6 +3587,30 @@
3587
3587
  }
3588
3588
  ]
3589
3589
  },
3590
+ {
3591
+ "kind": "method",
3592
+ "name": "_handleRowMouseDown",
3593
+ "privacy": "private"
3594
+ },
3595
+ {
3596
+ "kind": "method",
3597
+ "name": "_handleRowClick",
3598
+ "privacy": "private",
3599
+ "parameters": [
3600
+ {
3601
+ "name": "row",
3602
+ "type": {
3603
+ "text": "Record<string, any>"
3604
+ }
3605
+ },
3606
+ {
3607
+ "name": "rowIndex",
3608
+ "type": {
3609
+ "text": "number"
3610
+ }
3611
+ }
3612
+ ]
3613
+ },
3590
3614
  {
3591
3615
  "kind": "field",
3592
3616
  "name": "_handleClickOutsideColumnPicker",
@@ -3789,6 +3813,12 @@
3789
3813
  }
3790
3814
  ],
3791
3815
  "events": [
3816
+ {
3817
+ "name": "row-click",
3818
+ "type": {
3819
+ "text": "CustomEvent"
3820
+ }
3821
+ },
3792
3822
  {
3793
3823
  "name": "action",
3794
3824
  "type": {
@@ -2721,6 +2721,28 @@ let KRTable = class KRTable extends i$2 {
2721
2721
  this._displayedColumns = [...this._displayedColumns, columnId];
2722
2722
  }
2723
2723
  }
2724
+ // Clear any existing text selection on mousedown so we only detect
2725
+ // selections made during this click gesture, not stale selections from elsewhere
2726
+ _handleRowMouseDown() {
2727
+ if (!this._def.rowClickable) {
2728
+ return;
2729
+ }
2730
+ window.getSelection()?.removeAllRanges();
2731
+ }
2732
+ _handleRowClick(row, rowIndex) {
2733
+ if (!this._def.rowClickable) {
2734
+ return;
2735
+ }
2736
+ const selection = window.getSelection();
2737
+ if (selection && selection.toString().length > 0) {
2738
+ return;
2739
+ }
2740
+ this.dispatchEvent(new CustomEvent('row-click', {
2741
+ detail: { row, rowIndex },
2742
+ bubbles: true,
2743
+ composed: true
2744
+ }));
2745
+ }
2724
2746
  // When a user toggles a column on via the column picker, it gets appended
2725
2747
  // to _displayedColumns. By mapping over _displayedColumns (not def.columns),
2726
2748
  // the new column appears at the right edge of the table instead of jumping
@@ -3052,7 +3074,11 @@ let KRTable = class KRTable extends i$2 {
3052
3074
  `)}
3053
3075
  </div>
3054
3076
  ${this._data.map((row, rowIndex) => b `
3055
- <div class="row">
3077
+ <div
3078
+ class=${e$1({ 'row': true, 'row--clickable': !!this._def.rowClickable })}
3079
+ @mousedown=${() => this._handleRowMouseDown()}
3080
+ @click=${() => this._handleRowClick(row, rowIndex)}
3081
+ >
3056
3082
  ${this.getDisplayedColumns().map((col, i) => b `
3057
3083
  <div
3058
3084
  class=${e$1(this._getCellClasses(col, i))}
@@ -3408,6 +3434,10 @@ KRTable.styles = [krBaseCSS, i$5 `
3408
3434
  background: #f9fafb;
3409
3435
  }
3410
3436
 
3437
+ .row--clickable {
3438
+ cursor: pointer;
3439
+ }
3440
+
3411
3441
  .header-row {
3412
3442
  display: contents;
3413
3443
  }