snice 7.8.0 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.js +1 -1
- package/adapters/react/matchRoute.js +1 -1
- package/adapters/react/table.d.ts +1 -0
- package/adapters/react/table.d.ts.map +1 -1
- package/adapters/react/table.js +1 -1
- package/adapters/react/table.js.map +1 -1
- package/adapters/react/table.tsx +2 -1
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +17 -1
- package/custom-elements.json +57 -3
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/action-bar/snice-action-bar.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/binpack/snice-binpack.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/calendar/README.md +2 -2
- package/dist/cdn/calendar/snice-calendar.js +537 -44
- package/dist/cdn/calendar/snice-calendar.js.map +1 -1
- package/dist/cdn/calendar/snice-calendar.min.js +2 -2
- package/dist/cdn/calendar/snice-calendar.min.js.map +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/code-block/README.md +2 -2
- package/dist/cdn/code-block/snice-code-block.js +177 -167
- package/dist/cdn/code-block/snice-code-block.js.map +1 -1
- package/dist/cdn/code-block/snice-code-block.min.js +2 -2
- package/dist/cdn/code-block/snice-code-block.min.js.map +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/command-palette/README.md +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +2 -2
- package/dist/cdn/command-palette/snice-command-palette.js.map +1 -1
- package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.min.js.map +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/grid/snice-grid.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/message-strip/snice-message-strip.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/popover/snice-popover.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/progress/snice-progress.js +2 -2
- package/dist/cdn/progress/snice-progress.js.map +1 -1
- package/dist/cdn/progress/snice-progress.min.js +1 -1
- package/dist/cdn/progress/snice-progress.min.js.map +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/qr-code/README.md +2 -2
- package/dist/cdn/qr-code/snice-qr-code.js +19 -3
- package/dist/cdn/qr-code/snice-qr-code.js.map +1 -1
- package/dist/cdn/qr-code/snice-qr-code.min.js +2 -2
- package/dist/cdn/qr-code/snice-qr-code.min.js.map +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +5 -2
- package/dist/cdn/receipt/snice-receipt.js.map +1 -1
- package/dist/cdn/receipt/snice-receipt.min.js +1 -1
- package/dist/cdn/receipt/snice-receipt.min.js.map +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.js +4 -4
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +4 -4
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/select/README.md +2 -2
- package/dist/cdn/select/snice-select.js +6 -9
- package/dist/cdn/select/snice-select.js.map +1 -1
- package/dist/cdn/select/snice-select.min.js +2 -2
- package/dist/cdn/select/snice-select.min.js.map +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/table/README.md +2 -2
- package/dist/cdn/table/snice-table.esm.js +1940 -407
- package/dist/cdn/table/snice-table.esm.js.map +1 -1
- package/dist/cdn/table/snice-table.esm.min.js +242 -53
- package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
- package/dist/cdn/table/snice-table.js +1940 -407
- package/dist/cdn/table/snice-table.js.map +1 -1
- package/dist/cdn/table/snice-table.min.js +234 -45
- package/dist/cdn/table/snice-table.min.js.map +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tag/snice-tag.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/components/calendar/snice-calendar.d.ts +187 -5
- package/dist/components/calendar/snice-calendar.js +537 -44
- package/dist/components/calendar/snice-calendar.js.map +1 -1
- package/dist/components/calendar/snice-calendar.types.d.ts +2 -1
- package/dist/components/code-block/formatter.d.ts +6 -0
- package/dist/components/code-block/formatter.js +174 -164
- package/dist/components/code-block/formatter.js.map +1 -1
- package/dist/components/code-block/grammars/snice.json +1 -1
- package/dist/components/code-block/grammars/typescript.json +1 -1
- package/dist/components/command-palette/snice-command-palette.js +1 -1
- package/dist/components/command-palette/snice-command-palette.js.map +1 -1
- package/dist/components/progress/snice-progress.js +1 -1
- package/dist/components/progress/snice-progress.js.map +1 -1
- package/dist/components/qr-code/qrcode.d.ts +1 -1
- package/dist/components/qr-code/qrcode.js +18 -2
- package/dist/components/qr-code/qrcode.js.map +1 -1
- package/dist/components/receipt/snice-receipt.js +4 -1
- package/dist/components/receipt/snice-receipt.js.map +1 -1
- package/dist/components/select/snice-select.d.ts +1 -1
- package/dist/components/select/snice-select.js +5 -8
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/table/snice-cell-actions.js +1 -1
- package/dist/components/table/snice-cell-actions.js.map +1 -1
- package/dist/components/table/snice-cell-color.js +1 -1
- package/dist/components/table/snice-cell-color.js.map +1 -1
- package/dist/components/table/snice-cell-date.js +20 -8
- package/dist/components/table/snice-cell-date.js.map +1 -1
- package/dist/components/table/snice-cell-email.js +1 -1
- package/dist/components/table/snice-cell-email.js.map +1 -1
- package/dist/components/table/snice-cell-image.js +1 -1
- package/dist/components/table/snice-cell-image.js.map +1 -1
- package/dist/components/table/snice-cell-json.js +1 -1
- package/dist/components/table/snice-cell-json.js.map +1 -1
- package/dist/components/table/snice-cell-link.js +1 -1
- package/dist/components/table/snice-cell-link.js.map +1 -1
- package/dist/components/table/snice-cell-location.js +1 -1
- package/dist/components/table/snice-cell-location.js.map +1 -1
- package/dist/components/table/snice-cell-phone.js +1 -1
- package/dist/components/table/snice-cell-phone.js.map +1 -1
- package/dist/components/table/snice-cell-status.js +1 -1
- package/dist/components/table/snice-cell-status.js.map +1 -1
- package/dist/components/table/snice-cell.d.ts +10 -0
- package/dist/components/table/snice-cell.js +59 -13
- package/dist/components/table/snice-cell.js.map +1 -1
- package/dist/components/table/snice-row.d.ts +0 -1
- package/dist/components/table/snice-row.js +4 -10
- package/dist/components/table/snice-row.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +170 -2
- package/dist/components/table/snice-table.js +868 -189
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/snice-table.types.d.ts +17 -0
- package/dist/components/table/table-cell-presentation.d.ts +17 -4
- package/dist/components/table/table-cell-presentation.js +37 -11
- package/dist/components/table/table-cell-presentation.js.map +1 -1
- package/dist/components/table/table-column-manager.d.ts +153 -2
- package/dist/components/table/table-column-manager.js +364 -43
- package/dist/components/table/table-column-manager.js.map +1 -1
- package/dist/components/table/table-date.d.ts +29 -0
- package/dist/components/table/table-date.js +78 -0
- package/dist/components/table/table-date.js.map +1 -0
- package/dist/components/table/table-export.js +6 -2
- package/dist/components/table/table-export.js.map +1 -1
- package/dist/components/table/table-filter-engine.d.ts +9 -0
- package/dist/components/table/table-filter-engine.js +58 -14
- package/dist/components/table/table-filter-engine.js.map +1 -1
- package/dist/components/table/table-keyboard.d.ts +8 -0
- package/dist/components/table/table-keyboard.js +14 -3
- package/dist/components/table/table-keyboard.js.map +1 -1
- package/dist/components/table/table-toolbar.js +1 -0
- package/dist/components/table/table-toolbar.js.map +1 -1
- package/dist/components/table/table-tree-data.d.ts +2 -0
- package/dist/components/table/table-tree-data.js +35 -9
- package/dist/components/table/table-tree-data.js.map +1 -1
- package/dist/components/table/table-virtualizer.d.ts +44 -7
- package/dist/components/table/table-virtualizer.js +101 -54
- package/dist/components/table/table-virtualizer.js.map +1 -1
- package/dist/components/theme/theme.css +9 -0
- package/dist/index.cjs +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.iife.js +1 -1
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.js +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.js +1 -1
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/testing-dom.cjs +1 -1
- package/dist/testing-dom.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/docs/ai/DEVELOPMENT.md +93 -2
- package/docs/ai/components/calendar.md +113 -10
- package/docs/ai/components/code-block.md +4 -0
- package/docs/ai/components/doc.md +1 -1
- package/docs/ai/components/progress.md +1 -0
- package/docs/ai/components/receipt.md +1 -1
- package/docs/ai/components/table.md +29 -30
- package/docs/ai/components/theme.md +1 -0
- package/docs/ai/events.md +30 -0
- package/docs/ai/theme.md +1 -0
- package/docs/components/calendar.md +171 -11
- package/docs/components/code-block.md +2 -0
- package/docs/components/doc.md +1 -1
- package/docs/components/progress.md +13 -0
- package/docs/components/receipt.md +1 -0
- package/docs/components/table.md +274 -15
- package/docs/components/theme.md +5 -0
- package/docs/events.md +49 -0
- package/docs/plans/2026-03-06-date-range-picker-plan.md +4 -4
- package/docs/plans/2026-03-07-table-mui-parity-design.md +1 -1
- package/docs/plans/2026-03-09-action-bar-plan.md +11 -11
- package/docs/superpowers/specs/2026-07-09-table-incredible-design.md +1 -1
- package/docs/superpowers/specs/2026-07-09-table-phase0-implementation.md +2 -2
- package/package.json +3 -1
- package/vscode.html-custom-data.json +13 -1
|
@@ -6,6 +6,7 @@ import '../button/snice-button.js';
|
|
|
6
6
|
import '../checkbox/snice-checkbox.js';
|
|
7
7
|
import '../modal/snice-modal.js';
|
|
8
8
|
import '../empty-state/snice-empty-state.js';
|
|
9
|
+
import '../progress/snice-progress.js';
|
|
9
10
|
import './snice-cell.js';
|
|
10
11
|
import './snice-cell-text.js';
|
|
11
12
|
import './snice-cell-number.js';
|
|
@@ -31,7 +32,7 @@ import './snice-cell-image.js';
|
|
|
31
32
|
import './snice-column.js';
|
|
32
33
|
import './snice-row.js';
|
|
33
34
|
import { TableVirtualizer } from './table-virtualizer.js';
|
|
34
|
-
import { TableColumnManager } from './table-column-manager.js';
|
|
35
|
+
import { horizontalCellChrome, TableColumnManager } from './table-column-manager.js';
|
|
35
36
|
import { TableFilterEngine } from './table-filter-engine.js';
|
|
36
37
|
import { TableEditor } from './table-editor.js';
|
|
37
38
|
import { TableKeyboard } from './table-keyboard.js';
|
|
@@ -42,16 +43,22 @@ import { TableTreeData } from './table-tree-data.js';
|
|
|
42
43
|
import { TableGrouping } from './table-grouping.js';
|
|
43
44
|
import { TableColumnMenu } from './table-column-menu.js';
|
|
44
45
|
import { TableRowDnD, TableColumnDnD } from './table-row-dnd.js';
|
|
46
|
+
import { defaultCellAlign, emptyCellValue } from './table-cell-presentation.js';
|
|
45
47
|
import '../utils.js';
|
|
46
48
|
import '../icons/index.js';
|
|
47
49
|
import '../form-control-validity.js';
|
|
48
50
|
import '../form-label-association.js';
|
|
49
51
|
import '../select/snice-option.js';
|
|
50
52
|
import '../snice-cell-l6czK2XW.js';
|
|
51
|
-
import './table-
|
|
53
|
+
import './table-date.js';
|
|
52
54
|
import '../rating/snice-rating.js';
|
|
53
55
|
import './snice-progress.js';
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The one string both loading affordances use — the zero-row message row shows
|
|
59
|
+
* it, the refetch overlay announces it. Kept together so they can never drift.
|
|
60
|
+
*/
|
|
61
|
+
const LOADING_LABEL = 'Loading…';
|
|
55
62
|
let SniceTable = (() => {
|
|
56
63
|
let _classDecorators = [element('snice-table')];
|
|
57
64
|
let _classDescriptor;
|
|
@@ -146,6 +153,9 @@ let SniceTable = (() => {
|
|
|
146
153
|
let _density_decorators;
|
|
147
154
|
let _density_initializers = [];
|
|
148
155
|
let _density_extraInitializers = [];
|
|
156
|
+
let _columnFit_decorators;
|
|
157
|
+
let _columnFit_initializers = [];
|
|
158
|
+
let _columnFit_extraInitializers = [];
|
|
149
159
|
let _headerFilters_decorators;
|
|
150
160
|
let _headerFilters_initializers = [];
|
|
151
161
|
let _headerFilters_extraInitializers = [];
|
|
@@ -221,6 +231,7 @@ let SniceTable = (() => {
|
|
|
221
231
|
let _handleColumnReorderChange_decorators;
|
|
222
232
|
let _handleLazyLoadChange_decorators;
|
|
223
233
|
let _handleColumnMenuChange_decorators;
|
|
234
|
+
let _handleColumnFitChange_decorators;
|
|
224
235
|
let _dispatchPageChange_decorators;
|
|
225
236
|
let _dispatchRowSelectionChanged_decorators;
|
|
226
237
|
let _dispatchRowClicked_decorators;
|
|
@@ -258,8 +269,8 @@ let SniceTable = (() => {
|
|
|
258
269
|
_totalItems_decorators = [property({ type: Number, attribute: 'total-items' })];
|
|
259
270
|
_pageSizes_decorators = [property({ type: Array, attribute: false })];
|
|
260
271
|
_searchDebounce_decorators = [property({ type: Number, attribute: 'search-debounce' })];
|
|
261
|
-
_columns_decorators = [property({ attribute: false })];
|
|
262
|
-
_data_decorators = [property({ attribute: false })];
|
|
272
|
+
_columns_decorators = [property({ attribute: false, hasChanged: () => true })];
|
|
273
|
+
_data_decorators = [property({ attribute: false, hasChanged: () => true })];
|
|
263
274
|
_mode_decorators = [property()];
|
|
264
275
|
_currentSort_decorators = [property({ type: Array, attribute: false })];
|
|
265
276
|
_selector_decorators = [property({ type: String, attribute: 'selector' })];
|
|
@@ -272,6 +283,7 @@ let SniceTable = (() => {
|
|
|
272
283
|
_editable_decorators = [property({ type: Boolean, attribute: 'editable' })];
|
|
273
284
|
_editMode_decorators = [property({ attribute: 'edit-mode' })];
|
|
274
285
|
_density_decorators = [property({ attribute: 'density' })];
|
|
286
|
+
_columnFit_decorators = [property({ attribute: 'column-fit' })];
|
|
275
287
|
_headerFilters_decorators = [property({ type: Boolean, attribute: 'header-filters' })];
|
|
276
288
|
_quickFilter_decorators = [property({ type: Boolean, attribute: 'quick-filter' })];
|
|
277
289
|
_rowReorder_decorators = [property({ type: Boolean, attribute: 'row-reorder' })];
|
|
@@ -319,6 +331,7 @@ let SniceTable = (() => {
|
|
|
319
331
|
_handleColumnReorderChange_decorators = [watch('columnReorder', { immediate: false })];
|
|
320
332
|
_handleLazyLoadChange_decorators = [watch('lazyLoad', 'lazyLoadThreshold', { immediate: false })];
|
|
321
333
|
_handleColumnMenuChange_decorators = [watch('columnMenu', { immediate: false })];
|
|
334
|
+
_handleColumnFitChange_decorators = [watch('columnFit', { immediate: false })];
|
|
322
335
|
_dispatchPageChange_decorators = [dispatch('page-change', { bubbles: true, composed: true })];
|
|
323
336
|
_dispatchRowSelectionChanged_decorators = [dispatch('table-row-selection-changed', { bubbles: true, composed: true })];
|
|
324
337
|
_dispatchRowClicked_decorators = [dispatch('row-clicked', { bubbles: true, composed: true })];
|
|
@@ -368,6 +381,7 @@ let SniceTable = (() => {
|
|
|
368
381
|
__esDecorate(this, null, _handleColumnReorderChange_decorators, { kind: "method", name: "handleColumnReorderChange", static: false, private: false, access: { has: obj => "handleColumnReorderChange" in obj, get: obj => obj.handleColumnReorderChange }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
369
382
|
__esDecorate(this, null, _handleLazyLoadChange_decorators, { kind: "method", name: "handleLazyLoadChange", static: false, private: false, access: { has: obj => "handleLazyLoadChange" in obj, get: obj => obj.handleLazyLoadChange }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
370
383
|
__esDecorate(this, null, _handleColumnMenuChange_decorators, { kind: "method", name: "handleColumnMenuChange", static: false, private: false, access: { has: obj => "handleColumnMenuChange" in obj, get: obj => obj.handleColumnMenuChange }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
384
|
+
__esDecorate(this, null, _handleColumnFitChange_decorators, { kind: "method", name: "handleColumnFitChange", static: false, private: false, access: { has: obj => "handleColumnFitChange" in obj, get: obj => obj.handleColumnFitChange }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
371
385
|
__esDecorate(this, null, _dispatchPageChange_decorators, { kind: "method", name: "dispatchPageChange", static: false, private: false, access: { has: obj => "dispatchPageChange" in obj, get: obj => obj.dispatchPageChange }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
372
386
|
__esDecorate(this, null, _dispatchRowSelectionChanged_decorators, { kind: "method", name: "dispatchRowSelectionChanged", static: false, private: false, access: { has: obj => "dispatchRowSelectionChanged" in obj, get: obj => obj.dispatchRowSelectionChanged }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
373
387
|
__esDecorate(this, null, _dispatchRowClicked_decorators, { kind: "method", name: "dispatchRowClicked", static: false, private: false, access: { has: obj => "dispatchRowClicked" in obj, get: obj => obj.dispatchRowClicked }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -412,6 +426,7 @@ let SniceTable = (() => {
|
|
|
412
426
|
__esDecorate(null, null, _editable_decorators, { kind: "field", name: "editable", static: false, private: false, access: { has: obj => "editable" in obj, get: obj => obj.editable, set: (obj, value) => { obj.editable = value; } }, metadata: _metadata }, _editable_initializers, _editable_extraInitializers);
|
|
413
427
|
__esDecorate(null, null, _editMode_decorators, { kind: "field", name: "editMode", static: false, private: false, access: { has: obj => "editMode" in obj, get: obj => obj.editMode, set: (obj, value) => { obj.editMode = value; } }, metadata: _metadata }, _editMode_initializers, _editMode_extraInitializers);
|
|
414
428
|
__esDecorate(null, null, _density_decorators, { kind: "field", name: "density", static: false, private: false, access: { has: obj => "density" in obj, get: obj => obj.density, set: (obj, value) => { obj.density = value; } }, metadata: _metadata }, _density_initializers, _density_extraInitializers);
|
|
429
|
+
__esDecorate(null, null, _columnFit_decorators, { kind: "field", name: "columnFit", static: false, private: false, access: { has: obj => "columnFit" in obj, get: obj => obj.columnFit, set: (obj, value) => { obj.columnFit = value; } }, metadata: _metadata }, _columnFit_initializers, _columnFit_extraInitializers);
|
|
415
430
|
__esDecorate(null, null, _headerFilters_decorators, { kind: "field", name: "headerFilters", static: false, private: false, access: { has: obj => "headerFilters" in obj, get: obj => obj.headerFilters, set: (obj, value) => { obj.headerFilters = value; } }, metadata: _metadata }, _headerFilters_initializers, _headerFilters_extraInitializers);
|
|
416
431
|
__esDecorate(null, null, _quickFilter_decorators, { kind: "field", name: "quickFilter", static: false, private: false, access: { has: obj => "quickFilter" in obj, get: obj => obj.quickFilter, set: (obj, value) => { obj.quickFilter = value; } }, metadata: _metadata }, _quickFilter_initializers, _quickFilter_extraInitializers);
|
|
417
432
|
__esDecorate(null, null, _rowReorder_decorators, { kind: "field", name: "rowReorder", static: false, private: false, access: { has: obj => "rowReorder" in obj, get: obj => obj.rowReorder, set: (obj, value) => { obj.rowReorder = value; } }, metadata: _metadata }, _rowReorder_initializers, _rowReorder_extraInitializers);
|
|
@@ -455,6 +470,11 @@ let SniceTable = (() => {
|
|
|
455
470
|
// Wait for next frame to ensure DOM is updated
|
|
456
471
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
457
472
|
this.renderHeader();
|
|
473
|
+
// The body render the columns assignment queued above can have been
|
|
474
|
+
// consumed against a not-yet-committed <tbody>. Repainting the header
|
|
475
|
+
// without the rows is what leaves a remote table showing headings over an
|
|
476
|
+
// empty frame, so this pass owns both.
|
|
477
|
+
this.renderBody();
|
|
458
478
|
this.renderControls();
|
|
459
479
|
return config;
|
|
460
480
|
}
|
|
@@ -484,8 +504,7 @@ let SniceTable = (() => {
|
|
|
484
504
|
if (response.totalItems !== undefined) {
|
|
485
505
|
this.totalItems = response.totalItems;
|
|
486
506
|
}
|
|
487
|
-
this.
|
|
488
|
-
this.classList.remove('table--error');
|
|
507
|
+
this.clearLoadError();
|
|
489
508
|
this.loading = false;
|
|
490
509
|
// Wait for next frame to ensure DOM is updated
|
|
491
510
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
@@ -504,14 +523,49 @@ let SniceTable = (() => {
|
|
|
504
523
|
this.loading = false;
|
|
505
524
|
// Wait for next frame to ensure DOM is updated
|
|
506
525
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
526
|
+
// Same guard the success path applies above: a newer request can settle
|
|
527
|
+
// (and paint) inside this frame, and this failure's repaint would then be
|
|
528
|
+
// a second full body rebuild owed to nobody.
|
|
529
|
+
if (seq !== this.dataRequestSeq)
|
|
530
|
+
return;
|
|
507
531
|
this.renderBody();
|
|
508
532
|
}
|
|
509
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Requeue a body render whose target had not been committed yet. A frame is
|
|
536
|
+
* the right granularity — the shadow template commits between frames — and
|
|
537
|
+
* the paint is dropped again (silently, as before) once the retries run out.
|
|
538
|
+
*
|
|
539
|
+
* The retry CHAINS: a template that needs more than one frame to commit is
|
|
540
|
+
* the whole reason this exists, so a retry that still finds no <tbody> spends
|
|
541
|
+
* one more of the budget rather than giving the frame back and quitting.
|
|
542
|
+
*/
|
|
543
|
+
rearmBodyRender() {
|
|
544
|
+
if (this.bodyRenderRetryQueued || this.bodyRenderRetries >= 3)
|
|
545
|
+
return;
|
|
546
|
+
this.bodyRenderRetryQueued = true;
|
|
547
|
+
this.bodyRenderRetries++;
|
|
548
|
+
requestAnimationFrame(() => {
|
|
549
|
+
this.bodyRenderRetryQueued = false;
|
|
550
|
+
if (this.tbody)
|
|
551
|
+
this.renderBody();
|
|
552
|
+
else
|
|
553
|
+
this.rearmBodyRender();
|
|
554
|
+
});
|
|
555
|
+
}
|
|
510
556
|
scheduleRender(what) {
|
|
511
557
|
if (what === 'header' || what === 'both')
|
|
512
558
|
this.pendingHeaderRender = true;
|
|
513
|
-
if (what === 'body' || what === 'both')
|
|
559
|
+
if (what === 'body' || what === 'both') {
|
|
514
560
|
this.pendingBodyRender = true;
|
|
561
|
+
// A NEWLY requested paint gets a fresh budget. The bound exists to stop
|
|
562
|
+
// one render from spinning against a template that structurally has no
|
|
563
|
+
// <tbody> (the slotted-rows branch), not to disqualify the table forever:
|
|
564
|
+
// without this reset, three misses during the slotted phase silently
|
|
565
|
+
// cancel every body render that follows a later flip to the native table.
|
|
566
|
+
if (!this.bodyRenderRetryQueued)
|
|
567
|
+
this.bodyRenderRetries = 0;
|
|
568
|
+
}
|
|
515
569
|
if (this.renderQueued)
|
|
516
570
|
return;
|
|
517
571
|
this.renderQueued = true;
|
|
@@ -526,14 +580,28 @@ let SniceTable = (() => {
|
|
|
526
580
|
const doBody = this.pendingBodyRender;
|
|
527
581
|
this.pendingHeaderRender = false;
|
|
528
582
|
this.pendingBodyRender = false;
|
|
529
|
-
|
|
530
|
-
|
|
583
|
+
try {
|
|
584
|
+
if (doHeader)
|
|
585
|
+
this.renderHeader();
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
// Both flags are already consumed. Letting a header failure escape
|
|
589
|
+
// would cancel the body paint queued alongside it and leave the rows
|
|
590
|
+
// permanently stale — the exact "header fine, rows blank" symptom.
|
|
591
|
+
console.error('Error rendering table header:', error);
|
|
592
|
+
}
|
|
531
593
|
if (doBody)
|
|
532
594
|
this.renderBody();
|
|
533
595
|
});
|
|
534
596
|
});
|
|
535
597
|
}
|
|
536
598
|
debouncedDataRequest() {
|
|
599
|
+
// The supersede decision belongs to the moment the NEW request is decided
|
|
600
|
+
// on, not to the moment its timeout fires. Bumping here closes the 150ms
|
|
601
|
+
// window in which an already-obsolete in-flight response still satisfied
|
|
602
|
+
// `seq === this.dataRequestSeq` and painted rows for the previous sort /
|
|
603
|
+
// filter / page under a header that already advertises the new one.
|
|
604
|
+
this.dataRequestSeq++;
|
|
537
605
|
// Set loading immediately for instant feedback
|
|
538
606
|
if (!this.loading) {
|
|
539
607
|
this.loading = true;
|
|
@@ -621,6 +689,72 @@ let SniceTable = (() => {
|
|
|
621
689
|
/* no border or radius — handled by .table-frame */
|
|
622
690
|
}
|
|
623
691
|
|
|
692
|
+
/* column-fit="squish": the frame's width is the constraint.
|
|
693
|
+
*
|
|
694
|
+
* The arithmetic (syncColumnWidthsToFrame → fitVisibleColumns) already
|
|
695
|
+
* lands the columns on the frame's content width, and fixed layout hands
|
|
696
|
+
* any rounding slack back to the columns, so the rightmost edge sits on
|
|
697
|
+
* the frame's inner edge. These rules are the guarantee behind that:
|
|
698
|
+
* hiding the horizontal overflow means a stray pixel — a sub-pixel
|
|
699
|
+
* border, a browser rounding a percentage the other way — can never turn
|
|
700
|
+
* into a scrollbar the mode promised would not appear. Vertical scrolling
|
|
701
|
+
* is untouched. */
|
|
702
|
+
:host([column-fit="squish"]) .table-frame {
|
|
703
|
+
overflow-x: hidden;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
:host([column-fit="squish"]) table {
|
|
707
|
+
max-width: 100%;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* Squished columns are narrower than their content by design, so the
|
|
711
|
+
* content has to end in an ellipsis rather than wrap the row taller (or
|
|
712
|
+
* push a word past the clip edge). */
|
|
713
|
+
:host([column-fit="squish"]) th,
|
|
714
|
+
:host([column-fit="squish"]) td {
|
|
715
|
+
white-space: nowrap;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
:host([column-fit="squish"]) td > snice-cell-text,
|
|
719
|
+
:host([column-fit="squish"]) td > .custom-cell,
|
|
720
|
+
:host([column-fit="squish"]) td > .tree-label {
|
|
721
|
+
display: block;
|
|
722
|
+
overflow: hidden;
|
|
723
|
+
text-overflow: ellipsis;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/* The typed cells (email, status, link, phone, location, json, colour,
|
|
727
|
+
* image, actions) each declare a 100px floor on their host AND on their
|
|
728
|
+
* inner .cell-content. That floor is a sensible default at natural width
|
|
729
|
+
* and fatal here: a squished column is routinely narrower than 100px, the
|
|
730
|
+
* cell refuses to shrink to it, and the td's own overflow: hidden then
|
|
731
|
+
* chops the label mid-glyph — no ellipsis, because the element that owns
|
|
732
|
+
* the ellipsis never got small enough to trigger it.
|
|
733
|
+
*
|
|
734
|
+
* .cell-content lives in the cell's shadow root, so no descendant
|
|
735
|
+
* selector from here can reach it. A custom property can: it inherits
|
|
736
|
+
* across the boundary, the cells read it with the same 100px as the
|
|
737
|
+
* fallback, and nothing outside a squished td sees any change. */
|
|
738
|
+
:host([column-fit="squish"]) td {
|
|
739
|
+
--snice-table-cell-min-width: 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* The header label is the table's own markup and needs the same promise:
|
|
743
|
+
* "Department" clipped to "Departme" reads as a different word. A
|
|
744
|
+
* sortable header puts the label in a flex row beside the sort
|
|
745
|
+
* indicator, and a flex item only shrinks below its content once it is
|
|
746
|
+
* allowed to — hence min-width: 0 alongside the ellipsis. A plain header
|
|
747
|
+
* is bare text in the th, which ellipsises on the th itself. */
|
|
748
|
+
:host([column-fit="squish"]) th {
|
|
749
|
+
text-overflow: ellipsis;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
:host([column-fit="squish"]) th .sort-header > span {
|
|
753
|
+
overflow: hidden;
|
|
754
|
+
text-overflow: ellipsis;
|
|
755
|
+
min-width: 0;
|
|
756
|
+
}
|
|
757
|
+
|
|
624
758
|
/* Super-header (slotted area above column headers) */
|
|
625
759
|
.table-superheader {
|
|
626
760
|
background: var(--snice-color-surface, rgb(255 255 255));
|
|
@@ -743,9 +877,13 @@ let SniceTable = (() => {
|
|
|
743
877
|
background-color: var(--snice-color-surface-container-low, rgb(245 245 245));
|
|
744
878
|
}
|
|
745
879
|
|
|
746
|
-
/* Row styling
|
|
880
|
+
/* Row styling. The zebra tint is an ALPHA overlay, not a surface step:
|
|
881
|
+
surface-container-low sits ~2 luminance points from the body surface
|
|
882
|
+
on dark themes, so striped rendered as no striping at all there.
|
|
883
|
+
An overlay also composites over pinned/tinted cells instead of
|
|
884
|
+
replacing their color. */
|
|
747
885
|
:host([striped]) tbody tr:nth-child(even) {
|
|
748
|
-
background-color: var(--snice-color-
|
|
886
|
+
background-color: var(--snice-table-stripe-bg, var(--snice-color-overlay-stripe, hsl(0 0% 0% / 0.02)));
|
|
749
887
|
}
|
|
750
888
|
|
|
751
889
|
:host([hoverable]) tbody tr:hover {
|
|
@@ -1009,12 +1147,14 @@ let SniceTable = (() => {
|
|
|
1009
1147
|
justify-content: center;
|
|
1010
1148
|
}
|
|
1011
1149
|
|
|
1012
|
-
/* Loading fade
|
|
1013
|
-
|
|
1150
|
+
/* Loading fade. Scoped to ROWS, never the whole tbody: with no rows the
|
|
1151
|
+
only thing in there is the loading indicator, and dimming that halves
|
|
1152
|
+
the contrast of the one element whose entire job is to be seen. */
|
|
1153
|
+
tbody > tr {
|
|
1014
1154
|
transition: opacity var(--snice-transition-normal, 250ms);
|
|
1015
1155
|
}
|
|
1016
1156
|
|
|
1017
|
-
:host([loading]) tbody {
|
|
1157
|
+
:host([loading]) tbody > tr:not(.table-message-row) {
|
|
1018
1158
|
opacity: 0.5;
|
|
1019
1159
|
}
|
|
1020
1160
|
|
|
@@ -1029,6 +1169,33 @@ let SniceTable = (() => {
|
|
|
1029
1169
|
z-index: 2;
|
|
1030
1170
|
}
|
|
1031
1171
|
|
|
1172
|
+
/* Zero-row loading: spinner + the word, centred in the empty body. */
|
|
1173
|
+
.table-loading-state {
|
|
1174
|
+
display: flex;
|
|
1175
|
+
flex-direction: column;
|
|
1176
|
+
align-items: center;
|
|
1177
|
+
justify-content: center;
|
|
1178
|
+
gap: var(--snice-spacing-xs, 0.5rem);
|
|
1179
|
+
color: var(--snice-color-text-secondary, rgb(82 82 82));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.table-loading-label {
|
|
1183
|
+
font-size: var(--snice-font-size-sm, 0.875rem);
|
|
1184
|
+
line-height: 1;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/* snice-progress draws its circular track at 20% alpha, which disappears
|
|
1188
|
+
entirely on a dark surface — the ring the field report saw was a
|
|
1189
|
+
near-black hairline on near-black. Both table spinners opt out and use
|
|
1190
|
+
the border token at full strength, so the track reads in either
|
|
1191
|
+
theme and the primary arc always has something to travel around. */
|
|
1192
|
+
.table-loading-state snice-progress,
|
|
1193
|
+
.table-loading-overlay snice-progress {
|
|
1194
|
+
--progress-track-opacity: 1;
|
|
1195
|
+
--progress-bg: var(--snice-color-border, rgb(226 226 226));
|
|
1196
|
+
--progress-color: var(--snice-color-primary, rgb(37 99 235));
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1032
1199
|
.no-data {
|
|
1033
1200
|
text-align: center;
|
|
1034
1201
|
padding: var(--snice-spacing-lg, 1.5rem);
|
|
@@ -1170,10 +1337,58 @@ let SniceTable = (() => {
|
|
|
1170
1337
|
background: var(--snice-color-primary, rgb(37 99 235));
|
|
1171
1338
|
}
|
|
1172
1339
|
|
|
1173
|
-
/* Pinned
|
|
1340
|
+
/* Pinned columns.
|
|
1341
|
+
|
|
1342
|
+
A frozen column must be opaque — scrolled cells pass underneath it —
|
|
1343
|
+
and a header keeps the header's own surface rather than borrowing the
|
|
1344
|
+
body's. The frozen EDGE (the inner-most pinned column on each side) is
|
|
1345
|
+
what the reader actually needs to see: a heavier divider plus a soft
|
|
1346
|
+
shadow cast INTO the scrolling region, mirrored per side. Between two
|
|
1347
|
+
columns pinned to the same side there is no boundary to mark, so they
|
|
1348
|
+
keep the ordinary cell border.
|
|
1349
|
+
|
|
1350
|
+
The shadow is a border-token tint, not black: a black drop shadow is
|
|
1351
|
+
invisible against a dark surface, which is exactly how pinning came to
|
|
1352
|
+
have no affordance at all in the dark theme. */
|
|
1174
1353
|
.pinned-cell {
|
|
1175
1354
|
background: var(--snice-color-surface, rgb(255 255 255));
|
|
1176
|
-
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
th.pinned-cell {
|
|
1358
|
+
background: var(--snice-color-surface-container-low, rgb(245 245 245));
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.pinned-cell--left.pinned-cell--edge {
|
|
1362
|
+
border-right: 2px solid var(--snice-color-border, rgb(226 226 226));
|
|
1363
|
+
box-shadow: 6px 0 8px -6px var(--snice-color-border, rgb(226 226 226));
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.pinned-cell--right.pinned-cell--edge {
|
|
1367
|
+
border-left: 2px solid var(--snice-color-border, rgb(226 226 226));
|
|
1368
|
+
box-shadow: -6px 0 8px -6px var(--snice-color-border, rgb(226 226 226));
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/* A right-pinned header is often the last cell in the row, where the
|
|
1372
|
+
shared rule above strips the right border. Its divider is on the left,
|
|
1373
|
+
so nothing to restore — but the edge border must survive that rule. */
|
|
1374
|
+
th:last-child.pinned-cell--right.pinned-cell--edge,
|
|
1375
|
+
td:last-child.pinned-cell--right.pinned-cell--edge {
|
|
1376
|
+
border-right: none;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/* Pin marker: the only cue when the table is wide enough not to scroll. */
|
|
1380
|
+
.pin-indicator {
|
|
1381
|
+
display: inline-flex;
|
|
1382
|
+
align-items: center;
|
|
1383
|
+
margin-left: var(--snice-spacing-2xs, 0.25rem);
|
|
1384
|
+
color: var(--snice-color-text-secondary, rgb(82 82 82));
|
|
1385
|
+
opacity: 0.65;
|
|
1386
|
+
vertical-align: middle;
|
|
1387
|
+
pointer-events: none;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
.pin-indicator--right {
|
|
1391
|
+
transform: scaleX(-1);
|
|
1177
1392
|
}
|
|
1178
1393
|
|
|
1179
1394
|
/* Density: compact */
|
|
@@ -1596,7 +1811,7 @@ let SniceTable = (() => {
|
|
|
1596
1811
|
}
|
|
1597
1812
|
|
|
1598
1813
|
:host([striped]) .snice-table--slotted .table-body::slotted(snice-row:nth-child(even)) {
|
|
1599
|
-
background-color: var(--snice-color-
|
|
1814
|
+
background-color: var(--snice-table-stripe-bg, var(--snice-color-overlay-stripe, hsl(0 0% 0% / 0.02)));
|
|
1600
1815
|
}
|
|
1601
1816
|
|
|
1602
1817
|
:host([hoverable]) .snice-table--slotted .table-body::slotted(snice-row:hover) {
|
|
@@ -1702,9 +1917,18 @@ let SniceTable = (() => {
|
|
|
1702
1917
|
}
|
|
1703
1918
|
container.appendChild(controlsDiv);
|
|
1704
1919
|
}
|
|
1705
|
-
// Re-derive the
|
|
1706
|
-
// fullscreen toggled)
|
|
1920
|
+
// Re-derive the frame-dependent layout whenever the frame's box changes
|
|
1921
|
+
// (host resized, fullscreen toggled): the columns share its width, and the
|
|
1922
|
+
// leftover space below the rows changes with its height.
|
|
1707
1923
|
handleFrameResize() {
|
|
1924
|
+
// Only when the width actually moved. Fitting rewrites header widths, which
|
|
1925
|
+
// can change row heights, which can flip the frame's vertical scrollbar and
|
|
1926
|
+
// call us straight back — the guard stops that becoming a cycle.
|
|
1927
|
+
const width = this.shadowRoot?.querySelector('.table-frame')?.clientWidth ?? 0;
|
|
1928
|
+
if (width > 0 && width !== this.lastFittedFrameWidth) {
|
|
1929
|
+
this.lastFittedFrameWidth = width;
|
|
1930
|
+
this.syncColumnWidthsToFrame();
|
|
1931
|
+
}
|
|
1708
1932
|
this.updateFillRow();
|
|
1709
1933
|
}
|
|
1710
1934
|
async initialize() {
|
|
@@ -1807,7 +2031,18 @@ let SniceTable = (() => {
|
|
|
1807
2031
|
}
|
|
1808
2032
|
}
|
|
1809
2033
|
this.data.splice(toIndex, 0, item);
|
|
1810
|
-
|
|
2034
|
+
// Reordering the SAME rows is not a delivery: a failed reload keeps its
|
|
2035
|
+
// error until data actually arrives, so a drag must not dismiss it.
|
|
2036
|
+
// Everything else the data watcher does (row index, detail prep, render)
|
|
2037
|
+
// is still wanted here — hence a narrow flag rather than
|
|
2038
|
+
// `settingSortedData`, which skips all of it.
|
|
2039
|
+
this.reorderingRows = true;
|
|
2040
|
+
try {
|
|
2041
|
+
this.data = [...this.data];
|
|
2042
|
+
}
|
|
2043
|
+
finally {
|
|
2044
|
+
this.reorderingRows = false;
|
|
2045
|
+
}
|
|
1811
2046
|
this.selectedRows = selectedRows
|
|
1812
2047
|
.map((row) => this.indexOfRow(row))
|
|
1813
2048
|
.filter((index) => index >= 0);
|
|
@@ -1921,7 +2156,17 @@ let SniceTable = (() => {
|
|
|
1921
2156
|
this.renderHeader(); // Re-render header to show/hide sort indicators
|
|
1922
2157
|
}
|
|
1923
2158
|
handleSelectableChange() {
|
|
1924
|
-
this.render(); //
|
|
2159
|
+
this.render(); // host template (the structural mode can change)
|
|
2160
|
+
// The checkbox tool column is structural: like the sibling selection-mode
|
|
2161
|
+
// watcher, a post-mount toggle has to rebuild the header (select-all th)
|
|
2162
|
+
// AND every row (row checkbox) — the host template render leaves the
|
|
2163
|
+
// imperatively built thead/tbody untouched, so without this the two
|
|
2164
|
+
// disagree and every data cell sits one column off its header. Rows are
|
|
2165
|
+
// never recycled stale across the toggle: computeStructuralSig folds in
|
|
2166
|
+
// `sel=`. Skipped before the first render — the initial paint reads
|
|
2167
|
+
// `selectable` itself.
|
|
2168
|
+
if (this.thead)
|
|
2169
|
+
this.scheduleRender('both');
|
|
1925
2170
|
queueMicrotask(() => this.syncSlottedSelectionState());
|
|
1926
2171
|
}
|
|
1927
2172
|
// C1: reactive columns. A new column set can change cell types/formatting
|
|
@@ -1930,8 +2175,12 @@ let SniceTable = (() => {
|
|
|
1930
2175
|
// construction is deferred to the coalescing queue (setter-stack landmine).
|
|
1931
2176
|
handleColumnsAssignment() {
|
|
1932
2177
|
this.renderedRows = new Map();
|
|
2178
|
+
// A new array is a NEW CONFIGURATION: its order / pinned / width / declared
|
|
2179
|
+
// set replace whatever the column model is holding, and states for columns
|
|
2180
|
+
// it no longer declares are dropped. initialize() is the render-path resync
|
|
2181
|
+
// that must NOT do this (it would undo a drag-resize on every paint).
|
|
1933
2182
|
if (this.columns.length > 0)
|
|
1934
|
-
this.columnManager.
|
|
2183
|
+
this.columnManager.applyConfiguration(this.columns, this);
|
|
1935
2184
|
// F: a column set change can add/remove an `aggregate` — resync the grouping
|
|
1936
2185
|
// model's aggregator list and drop its stale flattened snapshot.
|
|
1937
2186
|
this.grouping.setColumns(this.columns);
|
|
@@ -2004,9 +2253,21 @@ let SniceTable = (() => {
|
|
|
2004
2253
|
// and the row-index map, then renders the body. The internal sort path
|
|
2005
2254
|
// (sortLocalData) reassigns this.data with a re-sorted view of the SAME rows
|
|
2006
2255
|
// and owns its own snapshot/render — settingSortedData tells us to skip.
|
|
2007
|
-
handleDataAssignment() {
|
|
2256
|
+
handleDataAssignment(oldVal) {
|
|
2008
2257
|
if (this.settingSortedData)
|
|
2009
2258
|
return;
|
|
2259
|
+
// A load error describes the LAST delivery ATTEMPT, so a dataset arriving
|
|
2260
|
+
// from any source supersedes it — including the app falling back to local
|
|
2261
|
+
// rows (or to none) after the remote load failed. Clearing it only on the
|
|
2262
|
+
// remote success path left `loadError` and the `table--error` host class
|
|
2263
|
+
// alive forever, so the next zero-row render showed the stale ⚠️ where the
|
|
2264
|
+
// empty state belonged.
|
|
2265
|
+
//
|
|
2266
|
+
// A REORDER is not an arrival: the row-reorder handler republishes the same
|
|
2267
|
+
// rows in a new array, and dragging a row must not dismiss the ⚠️ the
|
|
2268
|
+
// contract says survives until data actually arrives.
|
|
2269
|
+
if (!this.reorderingRows)
|
|
2270
|
+
this.clearLoadError();
|
|
2010
2271
|
this.unsortedData = [...this.data];
|
|
2011
2272
|
// C3: rows delivered while a client-side sort is active must land in that
|
|
2012
2273
|
// sorted order — the currentSort watcher only fires when the sort itself
|
|
@@ -2015,11 +2276,25 @@ let SniceTable = (() => {
|
|
|
2015
2276
|
if (this.mode !== 'remote' && this.currentSort.length > 0)
|
|
2016
2277
|
this.applyLocalSortOrder();
|
|
2017
2278
|
this.rebuildRowIndex();
|
|
2279
|
+
// Re-delivering the SAME rows in a different order must not move the
|
|
2280
|
+
// selection to whatever row inherited the index (no-op for a new dataset).
|
|
2281
|
+
if (oldVal)
|
|
2282
|
+
this.remapSelectionAfterReorder(oldVal);
|
|
2018
2283
|
this.masterDetail.prepare(this.data, row => this.rowValueSignature(row));
|
|
2019
2284
|
if (this.settingDataImperative)
|
|
2020
2285
|
return; // imperative setData(): caller renders
|
|
2021
2286
|
this.scheduleRender('body');
|
|
2022
2287
|
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Drop the failed-load state: the ⚠️ message row's text and the `table--error`
|
|
2290
|
+
* host class always move together, so they are cleared together and from one
|
|
2291
|
+
* place. Idempotent, and deliberately does NOT render — every caller is either
|
|
2292
|
+
* mid-delivery (the data watcher renders) or already renders itself.
|
|
2293
|
+
*/
|
|
2294
|
+
clearLoadError() {
|
|
2295
|
+
this.loadError = null;
|
|
2296
|
+
this.classList.remove('table--error');
|
|
2297
|
+
}
|
|
2023
2298
|
handleDataChange() {
|
|
2024
2299
|
this.renderBody();
|
|
2025
2300
|
this.updateLoadingOverlay();
|
|
@@ -2039,11 +2314,12 @@ let SniceTable = (() => {
|
|
|
2039
2314
|
overlay = document.createElement('div');
|
|
2040
2315
|
overlay.className = 'table-loading-overlay';
|
|
2041
2316
|
overlay.setAttribute('part', 'loading-overlay');
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
overlay.
|
|
2317
|
+
// The overlay sits ON TOP of readable rows, so it carries its label as
|
|
2318
|
+
// an aria-label rather than visible text — but it must still announce.
|
|
2319
|
+
overlay.setAttribute('role', 'status');
|
|
2320
|
+
overlay.setAttribute('aria-live', 'polite');
|
|
2321
|
+
overlay.setAttribute('aria-label', LOADING_LABEL);
|
|
2322
|
+
overlay.appendChild(this.createLoadingSpinner('medium'));
|
|
2047
2323
|
frame.appendChild(overlay);
|
|
2048
2324
|
}
|
|
2049
2325
|
else if (!show && overlay) {
|
|
@@ -2106,11 +2382,26 @@ let SniceTable = (() => {
|
|
|
2106
2382
|
this.sortLocalData();
|
|
2107
2383
|
}
|
|
2108
2384
|
handleCurrentPageChange() {
|
|
2385
|
+
// A clamp written by the display model itself is not a navigation — see
|
|
2386
|
+
// clampCurrentPage().
|
|
2387
|
+
if (this.clampingPage)
|
|
2388
|
+
return;
|
|
2109
2389
|
if (this.paginationMode === 'server' && this.mode === 'remote')
|
|
2110
2390
|
this.debouncedDataRequest();
|
|
2111
2391
|
else
|
|
2112
2392
|
this.scheduleRender('body');
|
|
2113
2393
|
}
|
|
2394
|
+
clampCurrentPage(page) {
|
|
2395
|
+
if (page === this.currentPage)
|
|
2396
|
+
return;
|
|
2397
|
+
this.clampingPage = true;
|
|
2398
|
+
try {
|
|
2399
|
+
this.currentPage = page;
|
|
2400
|
+
}
|
|
2401
|
+
finally {
|
|
2402
|
+
this.clampingPage = false;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2114
2405
|
handlePageSizeChange() {
|
|
2115
2406
|
this.currentPage = 1;
|
|
2116
2407
|
if (this.paginationMode === 'server' && this.mode === 'remote')
|
|
@@ -2126,7 +2417,9 @@ let SniceTable = (() => {
|
|
|
2126
2417
|
}
|
|
2127
2418
|
handleListChange() {
|
|
2128
2419
|
this.renderedRows = new Map();
|
|
2129
|
-
|
|
2420
|
+
// The header is part of the mode: entering list mode with a renderer drops
|
|
2421
|
+
// the columnar header, and leaving it must bring the header back.
|
|
2422
|
+
this.scheduleRender('both');
|
|
2130
2423
|
}
|
|
2131
2424
|
handleEditableChange() {
|
|
2132
2425
|
if (this.editable)
|
|
@@ -2184,6 +2477,17 @@ let SniceTable = (() => {
|
|
|
2184
2477
|
this.initColumnMenu();
|
|
2185
2478
|
this.scheduleRender('header');
|
|
2186
2479
|
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Switching fit policy re-decides every unsized column's width, so the frame
|
|
2482
|
+
* has to be re-fitted from scratch: the cached width would otherwise make the
|
|
2483
|
+
* resize observer's "did the frame move?" guard swallow the very next pass.
|
|
2484
|
+
*/
|
|
2485
|
+
handleColumnFitChange() {
|
|
2486
|
+
this.columnManager.setFitMode(this.columnFit);
|
|
2487
|
+
this.lastFittedFrameWidth = 0;
|
|
2488
|
+
this.syncColumnWidthsToFrame();
|
|
2489
|
+
this.scheduleRender('header');
|
|
2490
|
+
}
|
|
2187
2491
|
/** Column definitions in their actual painted order. Column state owns
|
|
2188
2492
|
* visibility/order; pinned columns are partitioned to their physical edges
|
|
2189
2493
|
* so right-pinned cells do not overlap later unpinned columns. */
|
|
@@ -2200,9 +2504,124 @@ let SniceTable = (() => {
|
|
|
2200
2504
|
.map((state) => byKey.get(state.key))
|
|
2201
2505
|
.filter((column) => !!column);
|
|
2202
2506
|
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Cells one full-width row spans: every visible data column plus the tool
|
|
2509
|
+
* columns that precede them. Group, aggregate, detail, empty-state, and
|
|
2510
|
+
* virtual-spacer rows all measure themselves against this — a row that spans
|
|
2511
|
+
* MORE than the table has invents columns, and `table-layout: fixed` then
|
|
2512
|
+
* spends the frame's leftover width on the phantoms.
|
|
2513
|
+
*/
|
|
2514
|
+
getTotalColumnSpan() {
|
|
2515
|
+
const managed = this.columnManager.getAllStates();
|
|
2516
|
+
const dataColumns = managed.length > 0
|
|
2517
|
+
? this.columnManager.getVisibleColumns().length
|
|
2518
|
+
: this.columns.length;
|
|
2519
|
+
return dataColumns
|
|
2520
|
+
+ (this.hasSelectionColumn() ? 1 : 0)
|
|
2521
|
+
+ (this.masterDetail.isEnabled() ? 1 : 0)
|
|
2522
|
+
+ (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Re-fit the columns nobody sized to the frame they are painted in.
|
|
2526
|
+
*
|
|
2527
|
+
* A column width is a CONTENT width, so the frame's budget has to be reduced
|
|
2528
|
+
* by each column's own padding + border and by the fixed tool columns before
|
|
2529
|
+
* it can be shared out. Both are read off the live header — the previous
|
|
2530
|
+
* render's, on the first pass — rather than guessed, so the fitted table
|
|
2531
|
+
* lands on the frame's width instead of a few pixels past it.
|
|
2532
|
+
*
|
|
2533
|
+
* Widths are written straight onto the header cells: under fixed layout the
|
|
2534
|
+
* first row owns the column grid, so no body repaint is needed and this can
|
|
2535
|
+
* run inside a resize observer without a render cascade.
|
|
2536
|
+
*/
|
|
2537
|
+
syncColumnWidthsToFrame() {
|
|
2538
|
+
// The policy is the table's, the arithmetic is the manager's: hand it over
|
|
2539
|
+
// before every fit so a `columnFit` change is honoured on the next pass
|
|
2540
|
+
// whether it arrived as a property, an attribute, or an upgrade.
|
|
2541
|
+
this.columnManager.setFitMode(this.columnFit);
|
|
2542
|
+
const frame = this.shadowRoot?.querySelector('.table-frame');
|
|
2543
|
+
const headerRow = this.shadowRoot?.querySelector('thead tr.column-header-row');
|
|
2544
|
+
if (!frame || !headerRow)
|
|
2545
|
+
return false;
|
|
2546
|
+
const available = frame.clientWidth;
|
|
2547
|
+
// Layoutless DOM (or a display:none host): every measurement is 0 and
|
|
2548
|
+
// fitting to it would slam the columns to their minimums for nothing.
|
|
2549
|
+
if (!(available > 0))
|
|
2550
|
+
return false;
|
|
2551
|
+
const dataCells = Array.from(headerRow.querySelectorAll('th[data-key]'));
|
|
2552
|
+
// An active list renderer drops the columnar header entirely; there is no
|
|
2553
|
+
// column geometry to fit and no cell to measure the chrome from.
|
|
2554
|
+
if (dataCells.length === 0)
|
|
2555
|
+
return false;
|
|
2556
|
+
let reserved = 0;
|
|
2557
|
+
for (const cell of Array.from(headerRow.children)) {
|
|
2558
|
+
if (!cell.hasAttribute('data-key'))
|
|
2559
|
+
reserved += cell.getBoundingClientRect().width;
|
|
2560
|
+
}
|
|
2561
|
+
const chrome = horizontalCellChrome(dataCells[0]);
|
|
2562
|
+
if (!this.columnManager.fitVisibleColumns(available - reserved, chrome))
|
|
2563
|
+
return false;
|
|
2564
|
+
for (const cell of dataCells) {
|
|
2565
|
+
const state = this.columnManager.getState(cell.getAttribute('data-key') || '');
|
|
2566
|
+
if (state)
|
|
2567
|
+
cell.style.width = `${state.width}px`;
|
|
2568
|
+
}
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Sticky geometry plus the visible affordance for a pinned cell.
|
|
2573
|
+
*
|
|
2574
|
+
* Freezing a column is only communicated if the reader can see where the
|
|
2575
|
+
* frozen region ends, so the inner-most pinned column on each side carries
|
|
2576
|
+
* the divider (`--edge`); the header additionally carries a pin marker, which
|
|
2577
|
+
* is the only cue when the table happens to be wide enough not to scroll.
|
|
2578
|
+
*/
|
|
2579
|
+
applyPinnedCell(cell, key, state, zIndex) {
|
|
2580
|
+
if (state.pinned !== 'left' && state.pinned !== 'right')
|
|
2581
|
+
return;
|
|
2582
|
+
const side = state.pinned;
|
|
2583
|
+
const columns = side === 'left'
|
|
2584
|
+
? this.columnManager.getPinnedLeft()
|
|
2585
|
+
: this.columnManager.getPinnedRight();
|
|
2586
|
+
const offsets = side === 'left'
|
|
2587
|
+
? this.columnManager.getPinnedLeftOffsets()
|
|
2588
|
+
: this.columnManager.getPinnedRightOffsets();
|
|
2589
|
+
// The edge of the frozen region: the last left-pinned column, the first
|
|
2590
|
+
// right-pinned one. Inner boundaries between two pinned columns are just
|
|
2591
|
+
// ordinary cell borders.
|
|
2592
|
+
const edgeKey = side === 'left' ? columns[columns.length - 1]?.key : columns[0]?.key;
|
|
2593
|
+
cell.classList.add('pinned-cell', `pinned-cell--${side}`);
|
|
2594
|
+
if (key === edgeKey)
|
|
2595
|
+
cell.classList.add('pinned-cell--edge');
|
|
2596
|
+
cell.style.position = 'sticky';
|
|
2597
|
+
cell.style[side] = `${offsets.get(key) ?? 0}px`;
|
|
2598
|
+
cell.style.zIndex = zIndex;
|
|
2599
|
+
}
|
|
2600
|
+
/** The pin marker a frozen header carries, so pinning reads at any width. */
|
|
2601
|
+
static pinIndicator(side) {
|
|
2602
|
+
return `<span class="pin-indicator pin-indicator--${side}" aria-hidden="true">`
|
|
2603
|
+
+ `<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor">`
|
|
2604
|
+
+ `<path d="M9.5 1a1 1 0 0 0-.7 1.7l.3.3-3 3-2.4-.5a1 1 0 0 0-.9 1.7l3 3-3.6 3.6a.6.6 0 0 0 .8.8L6.6 12l3 3a1 1 0 0 0 1.7-.9l-.5-2.4 3-3 .3.3a1 1 0 0 0 1.4-1.4l-5.3-5.3A1 1 0 0 0 9.5 1Z"/>`
|
|
2605
|
+
+ `</svg></span>`;
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* List mode is only "list mode" once a renderer is installed: `list` alone is
|
|
2609
|
+
* a border treatment and still paints normal columnar cells. With a renderer
|
|
2610
|
+
* every row collapses to one full-width cell, so there is no column geometry
|
|
2611
|
+
* left for a header to label.
|
|
2612
|
+
*/
|
|
2613
|
+
usesListRowRenderer() {
|
|
2614
|
+
return !!(this.list && this.listRenderer);
|
|
2615
|
+
}
|
|
2203
2616
|
renderHeader() {
|
|
2204
2617
|
if (!this.thead)
|
|
2205
2618
|
return;
|
|
2619
|
+
// A card/directory list has no columns, so a columnar header would promise
|
|
2620
|
+
// fields that align with nothing. Suppress the data-column headers (and the
|
|
2621
|
+
// equally columnar group and header-filter rows) while a list renderer is
|
|
2622
|
+
// active; the tool columns below still have real body cells to head, so
|
|
2623
|
+
// select-all is kept rather than lost as collateral.
|
|
2624
|
+
const listRows = this.usesListRowRenderer();
|
|
2206
2625
|
const headerRow = document.createElement('tr');
|
|
2207
2626
|
headerRow.className = 'column-header-row';
|
|
2208
2627
|
// Tool column headers — must match createRow order
|
|
@@ -2240,7 +2659,7 @@ let SniceTable = (() => {
|
|
|
2240
2659
|
}
|
|
2241
2660
|
headerRow.appendChild(selectCell);
|
|
2242
2661
|
}
|
|
2243
|
-
const visibleColumns = this.getVisibleColumnDefinitions();
|
|
2662
|
+
const visibleColumns = listRows ? [] : this.getVisibleColumnDefinitions();
|
|
2244
2663
|
visibleColumns.forEach(column => {
|
|
2245
2664
|
const th = document.createElement('th');
|
|
2246
2665
|
th.setAttribute('data-key', column.key);
|
|
@@ -2257,23 +2676,10 @@ let SniceTable = (() => {
|
|
|
2257
2676
|
}
|
|
2258
2677
|
// Apply column width
|
|
2259
2678
|
const state = this.columnManager.getState(column.key);
|
|
2679
|
+
const pinnedSide = state?.pinned === 'left' || state?.pinned === 'right' ? state.pinned : null;
|
|
2260
2680
|
if (state) {
|
|
2261
2681
|
th.style.width = `${state.width}px`;
|
|
2262
|
-
|
|
2263
|
-
if (state.pinned === 'left') {
|
|
2264
|
-
const offsets = this.columnManager.getPinnedLeftOffsets();
|
|
2265
|
-
th.classList.add('pinned-cell');
|
|
2266
|
-
th.style.position = 'sticky';
|
|
2267
|
-
th.style.left = `${offsets.get(column.key) ?? 0}px`;
|
|
2268
|
-
th.style.zIndex = '2';
|
|
2269
|
-
}
|
|
2270
|
-
else if (state.pinned === 'right') {
|
|
2271
|
-
const offsets = this.columnManager.getPinnedRightOffsets();
|
|
2272
|
-
th.classList.add('pinned-cell');
|
|
2273
|
-
th.style.position = 'sticky';
|
|
2274
|
-
th.style.right = `${offsets.get(column.key) ?? 0}px`;
|
|
2275
|
-
th.style.zIndex = '2';
|
|
2276
|
-
}
|
|
2682
|
+
this.applyPinnedCell(th, column.key, state, '2');
|
|
2277
2683
|
}
|
|
2278
2684
|
if (this.sortable && column.sortable !== false) {
|
|
2279
2685
|
th.classList.add('sortable');
|
|
@@ -2281,11 +2687,22 @@ let SniceTable = (() => {
|
|
|
2281
2687
|
// A role=button must be focusable and named, or the keyboard cannot
|
|
2282
2688
|
// sort at all. Matches the declarative header in snice-header.ts.
|
|
2283
2689
|
th.setAttribute('tabindex', '0');
|
|
2284
|
-
|
|
2690
|
+
// Frozen columns say so: the divider is a visual cue only.
|
|
2691
|
+
th.setAttribute('aria-label', pinnedSide
|
|
2692
|
+
? `Sort by ${column.label}, pinned ${pinnedSide}`
|
|
2693
|
+
: `Sort by ${column.label}`);
|
|
2285
2694
|
th.innerHTML = this.renderSortableHeader(column);
|
|
2286
2695
|
}
|
|
2287
2696
|
else {
|
|
2288
2697
|
th.textContent = column.label;
|
|
2698
|
+
if (pinnedSide)
|
|
2699
|
+
th.setAttribute('aria-label', `${column.label}, pinned ${pinnedSide}`);
|
|
2700
|
+
}
|
|
2701
|
+
if (pinnedSide) {
|
|
2702
|
+
// Inside the label, not after it: `.sort-header` is a full-width flex
|
|
2703
|
+
// row, so a sibling would be pushed onto a second line.
|
|
2704
|
+
const label = th.querySelector('.sort-header > span') ?? th;
|
|
2705
|
+
label.insertAdjacentHTML('beforeend', SniceTable.pinIndicator(pinnedSide));
|
|
2289
2706
|
}
|
|
2290
2707
|
// Filter indicator
|
|
2291
2708
|
if (this.filterEngine.hasColumnFilter(column.key)) {
|
|
@@ -2338,7 +2755,7 @@ let SniceTable = (() => {
|
|
|
2338
2755
|
});
|
|
2339
2756
|
this.thead.innerHTML = '';
|
|
2340
2757
|
// Column groups header row (if any)
|
|
2341
|
-
const groups = this.columnManager.getColumnGroups();
|
|
2758
|
+
const groups = listRows ? [] : this.columnManager.getColumnGroups();
|
|
2342
2759
|
if (groups.length > 0) {
|
|
2343
2760
|
const groupRow = document.createElement('tr');
|
|
2344
2761
|
groupRow.className = 'column-group-row';
|
|
@@ -2360,9 +2777,12 @@ let SniceTable = (() => {
|
|
|
2360
2777
|
groupRow.appendChild(groupedHeaders.content);
|
|
2361
2778
|
this.thead.appendChild(groupRow);
|
|
2362
2779
|
}
|
|
2363
|
-
|
|
2780
|
+
// A list-mode header row with no tool columns has nothing to show; leaving
|
|
2781
|
+
// it out keeps the thead empty so it paints no strip above the cards.
|
|
2782
|
+
if (!listRows || headerRow.children.length > 0)
|
|
2783
|
+
this.thead.appendChild(headerRow);
|
|
2364
2784
|
// Header filter row (if enabled)
|
|
2365
|
-
if (this.headerFilters) {
|
|
2785
|
+
if (this.headerFilters && !listRows) {
|
|
2366
2786
|
const filterRow = document.createElement('tr');
|
|
2367
2787
|
filterRow.className = 'header-filter-row';
|
|
2368
2788
|
if (this.rowReorder && this.rowDnD.isEnabled()) {
|
|
@@ -2399,6 +2819,11 @@ let SniceTable = (() => {
|
|
|
2399
2819
|
});
|
|
2400
2820
|
this.thead.appendChild(filterRow);
|
|
2401
2821
|
}
|
|
2822
|
+
// The header is in the DOM, so its real chrome and tool-column widths are
|
|
2823
|
+
// measurable: fit the unsized columns to the frame before the browser
|
|
2824
|
+
// paints. Widths land on the header cells that were just created, so this
|
|
2825
|
+
// never re-enters renderHeader.
|
|
2826
|
+
this.syncColumnWidthsToFrame();
|
|
2402
2827
|
}
|
|
2403
2828
|
renderSortableHeader(column) {
|
|
2404
2829
|
const sortItem = this.currentSort.find(s => s.column === column.key);
|
|
@@ -2430,9 +2855,90 @@ let SniceTable = (() => {
|
|
|
2430
2855
|
</div>
|
|
2431
2856
|
`;
|
|
2432
2857
|
}
|
|
2858
|
+
/**
|
|
2859
|
+
* The single message row a zero-row body shows: loading spinner, load error,
|
|
2860
|
+
* or the empty state (the `empty-state` slot cloned fresh, else the default
|
|
2861
|
+
* placeholder). Returns null when there is data to render. Shared by the
|
|
2862
|
+
* ordinary and virtualized paths — the virtualizer wipes the tbody itself, so
|
|
2863
|
+
* it asks for this row rather than leaving a virtualized empty table blank.
|
|
2864
|
+
*/
|
|
2865
|
+
/**
|
|
2866
|
+
* The spinner both loading affordances draw. `snice-progress` paints its
|
|
2867
|
+
* track at 20% alpha, which vanishes against a dark surface — the table pins
|
|
2868
|
+
* `--progress-track-opacity` in its own stylesheet so the ring reads on any
|
|
2869
|
+
* theme; the indicator arc keeps the primary color.
|
|
2870
|
+
*/
|
|
2871
|
+
createLoadingSpinner(size) {
|
|
2872
|
+
const spinner = document.createElement('snice-progress');
|
|
2873
|
+
spinner.setAttribute('variant', 'circular');
|
|
2874
|
+
spinner.setAttribute('indeterminate', '');
|
|
2875
|
+
spinner.setAttribute('size', size);
|
|
2876
|
+
return spinner;
|
|
2877
|
+
}
|
|
2878
|
+
createEmptyBodyRow() {
|
|
2879
|
+
if (this.data.length > 0 || this.columns.length === 0)
|
|
2880
|
+
return null;
|
|
2881
|
+
const toolCols = (this.hasSelectionColumn() ? 1 : 0)
|
|
2882
|
+
+ (this.masterDetail.isEnabled() ? 1 : 0)
|
|
2883
|
+
+ (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
|
|
2884
|
+
const tr = document.createElement('tr');
|
|
2885
|
+
tr.className = 'table-message-row';
|
|
2886
|
+
const td = document.createElement('td');
|
|
2887
|
+
td.colSpan = this.columns.length + toolCols;
|
|
2888
|
+
td.className = 'no-data';
|
|
2889
|
+
tr.appendChild(td);
|
|
2890
|
+
if (this.loading) {
|
|
2891
|
+
// A bare ring in an otherwise empty rectangle reads as a rendering
|
|
2892
|
+
// artefact, not as "we are fetching your rows": at `small` it is 24px of
|
|
2893
|
+
// low-contrast stroke with nothing naming it. The zero-row body has the
|
|
2894
|
+
// space for a full-size spinner AND the word, and the pair is what makes
|
|
2895
|
+
// the state legible — visually and to a screen reader.
|
|
2896
|
+
const status = document.createElement('div');
|
|
2897
|
+
status.className = 'table-loading-state';
|
|
2898
|
+
status.setAttribute('role', 'status');
|
|
2899
|
+
status.setAttribute('aria-live', 'polite');
|
|
2900
|
+
status.appendChild(this.createLoadingSpinner('medium'));
|
|
2901
|
+
const label = document.createElement('span');
|
|
2902
|
+
label.className = 'table-loading-label';
|
|
2903
|
+
label.textContent = LOADING_LABEL;
|
|
2904
|
+
status.appendChild(label);
|
|
2905
|
+
td.appendChild(status);
|
|
2906
|
+
}
|
|
2907
|
+
else if (this.loadError) {
|
|
2908
|
+
// Surfaces a failed remote load instead of silently falling through to
|
|
2909
|
+
// the generic "No data" empty state.
|
|
2910
|
+
const errorDiv = document.createElement('div');
|
|
2911
|
+
errorDiv.className = 'table-error-message';
|
|
2912
|
+
errorDiv.textContent = `⚠️ ${this.loadError}`;
|
|
2913
|
+
td.appendChild(errorDiv);
|
|
2914
|
+
}
|
|
2915
|
+
else {
|
|
2916
|
+
const slotted = this.querySelector('[slot="empty-state"]');
|
|
2917
|
+
if (slotted) {
|
|
2918
|
+
td.appendChild(slotted.cloneNode(true));
|
|
2919
|
+
}
|
|
2920
|
+
else {
|
|
2921
|
+
const empty = document.createElement('snice-empty-state');
|
|
2922
|
+
empty.setAttribute('size', 'small');
|
|
2923
|
+
empty.setAttribute('icon', '📭');
|
|
2924
|
+
empty.setAttribute('title', 'No data');
|
|
2925
|
+
empty.setAttribute('description', 'No records to display.');
|
|
2926
|
+
td.appendChild(empty);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
return tr;
|
|
2930
|
+
}
|
|
2433
2931
|
renderBody() {
|
|
2434
|
-
|
|
2932
|
+
// The queue flush consumed `pendingBodyRender` before calling us, so a body
|
|
2933
|
+
// render that arrives while the shadow template has not committed its
|
|
2934
|
+
// <tbody> (a `table/config` response landing before @ready, a structural
|
|
2935
|
+
// branch flip mid-flight) would simply vanish — the header repaints and the
|
|
2936
|
+
// rows stay stale forever. Re-arm it for the next frame instead.
|
|
2937
|
+
if (!this.tbody) {
|
|
2938
|
+
this.rearmBodyRender();
|
|
2435
2939
|
return;
|
|
2940
|
+
}
|
|
2941
|
+
this.bodyRenderRetries = 0;
|
|
2436
2942
|
// Update column manager when columns change
|
|
2437
2943
|
if (this.columns.length > 0) {
|
|
2438
2944
|
this.columnManager.initialize(this.columns, this);
|
|
@@ -2464,8 +2970,7 @@ let SniceTable = (() => {
|
|
|
2464
2970
|
// NOT the raw filtered data — otherwise the virtualizer windows the wrong
|
|
2465
2971
|
// dataset and expand/collapse silently no-ops.
|
|
2466
2972
|
if (this.virtualize && this.virtualizer.isEnabled()) {
|
|
2467
|
-
this.
|
|
2468
|
-
this.virtualizer.setTotalRows(this.virtualRowsSnapshot.length);
|
|
2973
|
+
this.syncVirtualRows();
|
|
2469
2974
|
// The virtualizer's afterRender hook re-establishes grid ARIA/focus on
|
|
2470
2975
|
// the freshly inserted window.
|
|
2471
2976
|
// Still render pagination
|
|
@@ -2474,64 +2979,15 @@ let SniceTable = (() => {
|
|
|
2474
2979
|
this.updateSelectAllState();
|
|
2475
2980
|
return;
|
|
2476
2981
|
}
|
|
2477
|
-
|
|
2982
|
+
const emptyBodyRow = this.createEmptyBodyRow();
|
|
2983
|
+
if (emptyBodyRow) {
|
|
2478
2984
|
// Empty / loading / error states replace the body with a single message
|
|
2479
2985
|
// row. Reset the recycler map so a later data render starts from a clean
|
|
2480
2986
|
// slate (no stale reuse candidates pointing at removed rows).
|
|
2481
2987
|
this.tbody.innerHTML = '';
|
|
2482
2988
|
this.renderedRows = new Map();
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
+ (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
|
|
2486
|
-
const colSpan = this.columns.length + toolCols;
|
|
2487
|
-
if (this.loading) {
|
|
2488
|
-
// Show loading spinner
|
|
2489
|
-
const tr = document.createElement('tr');
|
|
2490
|
-
const td = document.createElement('td');
|
|
2491
|
-
td.colSpan = colSpan;
|
|
2492
|
-
td.className = 'no-data';
|
|
2493
|
-
td.innerHTML = '<snice-progress variant="circular" indeterminate size="small"></snice-progress>';
|
|
2494
|
-
tr.appendChild(td);
|
|
2495
|
-
this.tbody.appendChild(tr);
|
|
2496
|
-
return;
|
|
2497
|
-
}
|
|
2498
|
-
else if (this.loadError) {
|
|
2499
|
-
// Show error state — surfaces a failed remote load instead of
|
|
2500
|
-
// silently falling through to the generic "No data" empty state.
|
|
2501
|
-
const tr = document.createElement('tr');
|
|
2502
|
-
const td = document.createElement('td');
|
|
2503
|
-
td.colSpan = colSpan;
|
|
2504
|
-
td.className = 'no-data';
|
|
2505
|
-
const errorDiv = document.createElement('div');
|
|
2506
|
-
errorDiv.className = 'table-error-message';
|
|
2507
|
-
errorDiv.textContent = `⚠️ ${this.loadError}`;
|
|
2508
|
-
td.appendChild(errorDiv);
|
|
2509
|
-
tr.appendChild(td);
|
|
2510
|
-
this.tbody.appendChild(tr);
|
|
2511
|
-
return;
|
|
2512
|
-
}
|
|
2513
|
-
else {
|
|
2514
|
-
// Show empty state
|
|
2515
|
-
const tr = document.createElement('tr');
|
|
2516
|
-
const td = document.createElement('td');
|
|
2517
|
-
td.colSpan = colSpan;
|
|
2518
|
-
td.className = 'no-data';
|
|
2519
|
-
const slotted = this.querySelector('[slot="empty-state"]');
|
|
2520
|
-
if (slotted) {
|
|
2521
|
-
td.appendChild(slotted.cloneNode(true));
|
|
2522
|
-
}
|
|
2523
|
-
else {
|
|
2524
|
-
const empty = document.createElement('snice-empty-state');
|
|
2525
|
-
empty.setAttribute('size', 'small');
|
|
2526
|
-
empty.setAttribute('icon', '📭');
|
|
2527
|
-
empty.setAttribute('title', 'No data');
|
|
2528
|
-
empty.setAttribute('description', 'No records to display.');
|
|
2529
|
-
td.appendChild(empty);
|
|
2530
|
-
}
|
|
2531
|
-
tr.appendChild(td);
|
|
2532
|
-
this.tbody.appendChild(tr);
|
|
2533
|
-
return;
|
|
2534
|
-
}
|
|
2989
|
+
this.tbody.appendChild(emptyBodyRow);
|
|
2990
|
+
return;
|
|
2535
2991
|
}
|
|
2536
2992
|
// Apply client-side filters (cached snapshot — see getFilteredData).
|
|
2537
2993
|
const filteredData = this.getFilteredData();
|
|
@@ -2543,15 +2999,23 @@ let SniceTable = (() => {
|
|
|
2543
2999
|
if (this.pagination && this.paginationMode === 'client'
|
|
2544
3000
|
&& !this.usesGroupingDisplayModel() && !this.treeData.isEnabled()) {
|
|
2545
3001
|
this.totalItems = filteredData.length;
|
|
2546
|
-
|
|
3002
|
+
// Re-clamp before slicing. A shorter re-delivery (or a filter) can leave
|
|
3003
|
+
// `currentPage` past the new last page; slicing at the stale offset lands
|
|
3004
|
+
// off the end and renders an EMPTY body while the pager still advertises
|
|
3005
|
+
// the tail rows — blank until the user clicks a page button. Same
|
|
3006
|
+
// [1, totalPages] clamp goToPage() applies, and the same one the grouping
|
|
3007
|
+
// and tree display models already do before their own slice.
|
|
3008
|
+
// An empty set is left alone: a render before the rows arrive must not
|
|
3009
|
+
// rewrite the host's declared starting page.
|
|
3010
|
+
const totalPages = filteredData.length > 0
|
|
3011
|
+
? Math.ceil(filteredData.length / this.pageSize)
|
|
3012
|
+
: this.currentPage;
|
|
3013
|
+
const page = Math.max(1, Math.min(this.currentPage, totalPages));
|
|
3014
|
+
this.clampCurrentPage(page);
|
|
3015
|
+
const startIndex = (page - 1) * this.pageSize;
|
|
2547
3016
|
displayData = filteredData.slice(startIndex, startIndex + this.pageSize);
|
|
2548
3017
|
}
|
|
2549
|
-
const
|
|
2550
|
-
const managedColumns = this.columnManager.getAllStates();
|
|
2551
|
-
const visibleColumnCount = managedColumns.length > 0
|
|
2552
|
-
? this.columnManager.getVisibleColumns().length
|
|
2553
|
-
: this.columns.length;
|
|
2554
|
-
const totalColSpan = visibleColumnCount + extraCols;
|
|
3018
|
+
const totalColSpan = this.getTotalColumnSpan();
|
|
2555
3019
|
const structuralSig = this.computeStructuralSig();
|
|
2556
3020
|
// Task B: describe the desired body as an ordered list of keyed entries,
|
|
2557
3021
|
// then reconcile the tbody to match. Rows whose object + signature are
|
|
@@ -2647,6 +3111,19 @@ let SniceTable = (() => {
|
|
|
2647
3111
|
restampIndex: index,
|
|
2648
3112
|
create: () => this.createRow(rowData, index, treeRow),
|
|
2649
3113
|
});
|
|
3114
|
+
// Tree data must compose with master-detail exactly like the ordinary
|
|
3115
|
+
// and grouping paths: createRow renders the toggle for every tree row,
|
|
3116
|
+
// so omitting this entry left that control inert. Generated gap nodes
|
|
3117
|
+
// (index -1) carry no data row and therefore no panel.
|
|
3118
|
+
if (index >= 0 && this.masterDetail.isEnabled() && this.masterDetail.isExpanded(index)) {
|
|
3119
|
+
entries.push({
|
|
3120
|
+
key: `__detail_${index}`,
|
|
3121
|
+
sig: 'detail',
|
|
3122
|
+
alwaysRebuild: true,
|
|
3123
|
+
create: () => this.masterDetail.createDetailRow(rowData, index, totalColSpan) ??
|
|
3124
|
+
document.createElement('tr'),
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
2650
3127
|
});
|
|
2651
3128
|
}
|
|
2652
3129
|
else {
|
|
@@ -2704,6 +3181,20 @@ let SniceTable = (() => {
|
|
|
2704
3181
|
* data row, so column widths, pinning, and alignment track the table
|
|
2705
3182
|
* engine natively. Removed entirely when content fills (or overflows) the
|
|
2706
3183
|
* frame, restoring the plain last-row styling.
|
|
3184
|
+
*
|
|
3185
|
+
* INVARIANT — the filler may only consume space the frame ALREADY has; space
|
|
3186
|
+
* that exists only BECAUSE of the filler must be given back. The filler is
|
|
3187
|
+
* table content, and `@observe('resize', '.table-frame')` re-derives it from
|
|
3188
|
+
* the frame's box, so on a host whose height is content-driven (the
|
|
3189
|
+
* documented `:host { height: 100% }` resolving against an auto or
|
|
3190
|
+
* max-content container — a stretched grid/flex item, a plain block parent)
|
|
3191
|
+
* the filler is an input to the very measurement that produced it: the frame
|
|
3192
|
+
* grows, the observer fires, the leftover computes positive again, and the
|
|
3193
|
+
* table inflates by one slack per animation frame without bound. No static
|
|
3194
|
+
* check distinguishes a content-driven frame from a genuinely sized one, so
|
|
3195
|
+
* this measures instead: apply the filler, then confirm the frame kept its
|
|
3196
|
+
* height. Any growth is self-inflicted and is subtracted from the filler;
|
|
3197
|
+
* once nothing honest is left, the frame had no free space to fill at all.
|
|
2707
3198
|
*/
|
|
2708
3199
|
updateFillRow() {
|
|
2709
3200
|
const frame = this.shadowRoot?.querySelector('.table-frame');
|
|
@@ -2718,22 +3209,45 @@ let SniceTable = (() => {
|
|
|
2718
3209
|
const lastRow = rows[rows.length - 1];
|
|
2719
3210
|
if (!lastRow)
|
|
2720
3211
|
return;
|
|
2721
|
-
|
|
3212
|
+
// Baseline: the frame's height with no filler of ours in the box. Every
|
|
3213
|
+
// later reading is compared against it, so the correction below can never
|
|
3214
|
+
// hand back more than the filler itself created.
|
|
3215
|
+
const baseline = frame.clientHeight;
|
|
3216
|
+
let leftover = Math.floor(baseline - tableEl.offsetHeight);
|
|
2722
3217
|
if (leftover <= 1)
|
|
2723
3218
|
return;
|
|
2724
3219
|
const fill = lastRow.cloneNode(true);
|
|
2725
3220
|
fill.className = 'table-fill-row';
|
|
2726
|
-
|
|
3221
|
+
// `part` goes with the rest of the row identity: the filler is not "each
|
|
3222
|
+
// native body row", so a consumer's ::part(row)/::part(cell) rule must not
|
|
3223
|
+
// paint a phantom row across it.
|
|
3224
|
+
for (const attr of ['data-index', 'data-selected', 'role', 'aria-rowindex', 'aria-selected', 'tabindex', 'id', 'part']) {
|
|
2727
3225
|
fill.removeAttribute(attr);
|
|
2728
3226
|
}
|
|
2729
3227
|
fill.setAttribute('aria-hidden', 'true');
|
|
2730
3228
|
fill.querySelectorAll('td').forEach(td => {
|
|
2731
3229
|
td.replaceChildren();
|
|
2732
|
-
for (const attr of ['role', 'aria-colindex', 'tabindex', 'aria-selected'])
|
|
3230
|
+
for (const attr of ['role', 'aria-colindex', 'tabindex', 'aria-selected', 'part'])
|
|
2733
3231
|
td.removeAttribute(attr);
|
|
2734
3232
|
});
|
|
2735
3233
|
fill.style.height = `${leftover}px`;
|
|
2736
3234
|
tbody.appendChild(fill);
|
|
3235
|
+
// Feedback check. Reading `clientHeight` flushes layout, so each pass sees
|
|
3236
|
+
// the frame as it stands WITH the filler in it. A frame that kept its
|
|
3237
|
+
// baseline is genuinely sized and the filler stays; growth means the frame
|
|
3238
|
+
// is (partly) sized by its own content, so give exactly that back. Two
|
|
3239
|
+
// corrections settle every real case — a fully self-made leftover reaches
|
|
3240
|
+
// zero on the first — and the bounded loop guarantees termination.
|
|
3241
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3242
|
+
const selfMade = frame.clientHeight - baseline;
|
|
3243
|
+
if (selfMade <= 0)
|
|
3244
|
+
return;
|
|
3245
|
+
leftover -= selfMade;
|
|
3246
|
+
if (leftover <= 1)
|
|
3247
|
+
break;
|
|
3248
|
+
fill.style.height = `${leftover}px`;
|
|
3249
|
+
}
|
|
3250
|
+
fill.remove();
|
|
2737
3251
|
}
|
|
2738
3252
|
// ── Task B: render-path reconciler ──────────────────────────────────────
|
|
2739
3253
|
//
|
|
@@ -2891,7 +3405,7 @@ let SniceTable = (() => {
|
|
|
2891
3405
|
restampRowIndex(el, index) {
|
|
2892
3406
|
el.setAttribute('data-index', String(index));
|
|
2893
3407
|
const isSelected = this.selectedRows.includes(index);
|
|
2894
|
-
|
|
3408
|
+
this.stampRowSelection(el, isSelected);
|
|
2895
3409
|
const checkbox = el.querySelector('snice-checkbox.row-select');
|
|
2896
3410
|
if (checkbox) {
|
|
2897
3411
|
checkbox.setAttribute('data-row-index', String(index));
|
|
@@ -3078,24 +3592,30 @@ let SniceTable = (() => {
|
|
|
3078
3592
|
span.textContent = rendered == null ? '' : String(rendered);
|
|
3079
3593
|
return span;
|
|
3080
3594
|
}
|
|
3081
|
-
|
|
3595
|
+
// A declared display formatter resolves the whole pipeline here — the
|
|
3596
|
+
// documented precedence is `formatter` first, `valueFormatter` as the
|
|
3597
|
+
// fallback — and its output is TEXT, so the column renders through the text
|
|
3598
|
+
// cell: a rating/progress/link/boolean cell cannot carry a display string
|
|
3599
|
+
// (its value channel is typed) and renders structure rather than text. This
|
|
3600
|
+
// is the rule the declarative <snice-row> path already uses.
|
|
3601
|
+
const displayText = this.cellDisplayText(column, value, row);
|
|
3602
|
+
const tagName = displayText === null ? this.getCellTagName(column.type) : 'snice-cell-text';
|
|
3082
3603
|
const el = document.createElement(tagName);
|
|
3083
3604
|
// Base attributes. Only force `align` if the column definition specifies
|
|
3084
3605
|
// one; otherwise let the cell pick its type-appropriate default (numbers
|
|
3085
3606
|
// right, rating/boolean center, text left). Hard-coding 'left' here fought
|
|
3086
|
-
// the cell's own type-based text-align and made number values drift
|
|
3607
|
+
// the cell's own type-based text-align and made number values drift — and a
|
|
3608
|
+
// formatted column keeps its TYPE's default, not the text cell's.
|
|
3087
3609
|
el.setAttribute('type', column.type || 'text');
|
|
3088
3610
|
if (column.align)
|
|
3089
3611
|
el.setAttribute('align', column.align);
|
|
3612
|
+
else if (displayText !== null)
|
|
3613
|
+
el.setAttribute('align', defaultCellAlign(column.type));
|
|
3090
3614
|
el.setAttribute('in-table', 'true');
|
|
3091
|
-
//
|
|
3092
|
-
// the
|
|
3093
|
-
//
|
|
3094
|
-
// cell
|
|
3095
|
-
// without the formatters, which keeps it from formatting again and tells
|
|
3096
|
-
// the shared presentation contract to paint the text over any type-specific
|
|
3097
|
-
// rendering (a rating/progress/link cell cannot render display text).
|
|
3098
|
-
const displayText = this.cellDisplayText(column, value, row);
|
|
3615
|
+
// The cell's value IS its display value: with a formatter declared the cell
|
|
3616
|
+
// receives the formatted text and a column with the formatters removed, so
|
|
3617
|
+
// nothing formats twice and the `value` channel a caller reads is what the
|
|
3618
|
+
// cell shows.
|
|
3099
3619
|
const cellColumn = displayText === null
|
|
3100
3620
|
? column
|
|
3101
3621
|
: { ...column, formatter: undefined, valueFormatter: undefined };
|
|
@@ -3103,7 +3623,6 @@ let SniceTable = (() => {
|
|
|
3103
3623
|
// JSON, action, and tag cells without a lossy attribute conversion. Keep a
|
|
3104
3624
|
// primitive attribute for declarative inspection and CSS selectors.
|
|
3105
3625
|
if (displayText !== null) {
|
|
3106
|
-
el.setAttribute('formatted', '');
|
|
3107
3626
|
el.value = displayText;
|
|
3108
3627
|
el.setAttribute('value', displayText);
|
|
3109
3628
|
}
|
|
@@ -3113,9 +3632,12 @@ let SniceTable = (() => {
|
|
|
3113
3632
|
el.data = value;
|
|
3114
3633
|
}
|
|
3115
3634
|
else {
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3635
|
+
// An empty row value (null, or a field the row never carried) falls back
|
|
3636
|
+
// to the typed cell's own documented empty value, never to a blanket ''.
|
|
3637
|
+
const resolved = value ?? emptyCellValue(column.type);
|
|
3638
|
+
el.value = resolved;
|
|
3639
|
+
if (resolved !== null && typeof resolved !== 'object') {
|
|
3640
|
+
el.setAttribute('value', String(resolved));
|
|
3119
3641
|
}
|
|
3120
3642
|
}
|
|
3121
3643
|
// Column definition as property (for formatter access etc.)
|
|
@@ -3332,16 +3854,36 @@ let SniceTable = (() => {
|
|
|
3332
3854
|
updateRowSelectionState() {
|
|
3333
3855
|
if (!this.tbody)
|
|
3334
3856
|
return;
|
|
3335
|
-
|
|
3336
|
-
|
|
3857
|
+
// Key off each row's own `data-index`, never its DOM position: group
|
|
3858
|
+
// headers, aggregate footers, virtual spacers, and the inert fill row share
|
|
3859
|
+
// the tbody, so a positional walk hands a raw-data index to a non-data row
|
|
3860
|
+
// (and drifts every real row after it — on page 2 of a client-paged table
|
|
3861
|
+
// the offsets never lined up at all).
|
|
3862
|
+
const rows = this.tbody.querySelectorAll('tr[data-index]');
|
|
3863
|
+
rows.forEach(row => {
|
|
3864
|
+
const index = Number(row.getAttribute('data-index'));
|
|
3337
3865
|
const isSelected = this.selectedRows.includes(index);
|
|
3338
|
-
|
|
3866
|
+
this.stampRowSelection(row, isSelected);
|
|
3339
3867
|
const checkbox = row.querySelector('snice-checkbox.row-select');
|
|
3340
3868
|
if (checkbox) {
|
|
3341
3869
|
checkbox.checked = isSelected;
|
|
3342
3870
|
}
|
|
3343
3871
|
});
|
|
3344
3872
|
}
|
|
3873
|
+
/**
|
|
3874
|
+
* The two selection reflections a row carries, written together: `data-selected`
|
|
3875
|
+
* drives the CSS, `aria-selected` is the documented a11y contract ("selected
|
|
3876
|
+
* data rows expose aria-selected"). The keyboard module stamps ARIA only on a
|
|
3877
|
+
* full body render, so the delta paths below have to carry it themselves —
|
|
3878
|
+
* otherwise a screen reader never hears an interaction-driven selection.
|
|
3879
|
+
*/
|
|
3880
|
+
stampRowSelection(row, isSelected) {
|
|
3881
|
+
row.setAttribute('data-selected', String(isSelected));
|
|
3882
|
+
if (isSelected)
|
|
3883
|
+
row.setAttribute('aria-selected', 'true');
|
|
3884
|
+
else
|
|
3885
|
+
row.removeAttribute('aria-selected');
|
|
3886
|
+
}
|
|
3345
3887
|
// A2: reflect one row's selection into the DOM (located by data-index). The
|
|
3346
3888
|
// reconciler (Task B) re-stamps data-index on moved rows, so this stays
|
|
3347
3889
|
// correct after reorder. No-op if the row isn't in the current window.
|
|
@@ -3351,7 +3893,7 @@ let SniceTable = (() => {
|
|
|
3351
3893
|
const row = this.tbody.querySelector(`tr[data-index="${index}"]`);
|
|
3352
3894
|
if (!row)
|
|
3353
3895
|
return;
|
|
3354
|
-
|
|
3896
|
+
this.stampRowSelection(row, isSelected);
|
|
3355
3897
|
const checkbox = row.querySelector('snice-checkbox.row-select');
|
|
3356
3898
|
if (checkbox)
|
|
3357
3899
|
checkbox.checked = isSelected;
|
|
@@ -3452,14 +3994,54 @@ let SniceTable = (() => {
|
|
|
3452
3994
|
this.settingSortedData = false;
|
|
3453
3995
|
}
|
|
3454
3996
|
sortLocalData() {
|
|
3997
|
+
const previous = this.data;
|
|
3455
3998
|
this.applyLocalSortOrder();
|
|
3456
3999
|
// Sort replaced this.data (new order + reference) — resync the row-index
|
|
3457
4000
|
// map and drop the stale filtered snapshot before re-rendering. Render is
|
|
3458
4001
|
// queued (not synchronous): sortLocalData is reachable from the currentSort
|
|
3459
4002
|
// setter stack via handleControlledSort, and cells must never construct there.
|
|
3460
4003
|
this.rebuildRowIndex();
|
|
4004
|
+
this.remapSelectionAfterReorder(previous);
|
|
3461
4005
|
this.scheduleRender('body');
|
|
3462
4006
|
}
|
|
4007
|
+
/**
|
|
4008
|
+
* Selection is anchored to the ROW, not to its position. `selectedRows` holds
|
|
4009
|
+
* indices into `data`, so any reorder of that array — a local sort, or a
|
|
4010
|
+
* re-delivery of the same row objects in another order — has to re-resolve
|
|
4011
|
+
* them; otherwise the indices keep pointing at whatever row now occupies the
|
|
4012
|
+
* slot and the user's checkmark silently jumps to a different row (the same
|
|
4013
|
+
* remap the row-reorder/DnD path performs).
|
|
4014
|
+
*
|
|
4015
|
+
* Only a permutation is remapped: same length, same identities, no duplicates.
|
|
4016
|
+
* A genuinely new dataset keeps the documented raw-index semantics untouched,
|
|
4017
|
+
* and rows carrying no identity (duplicate objects) have nothing to anchor to.
|
|
4018
|
+
* Call AFTER rebuildRowIndex() — the remap reads the fresh row-index map.
|
|
4019
|
+
*/
|
|
4020
|
+
remapSelectionAfterReorder(previous) {
|
|
4021
|
+
if (this.selectedRows.length === 0)
|
|
4022
|
+
return;
|
|
4023
|
+
if (previous === this.data || previous.length !== this.data.length)
|
|
4024
|
+
return;
|
|
4025
|
+
const identities = new Set(this.data);
|
|
4026
|
+
if (identities.size !== this.data.length)
|
|
4027
|
+
return;
|
|
4028
|
+
// BOTH sides have to be duplicate-free for "same length + subset" to mean
|
|
4029
|
+
// permutation: previous=[A,A] against data=[A,B] satisfies the subset test
|
|
4030
|
+
// while actually being a NEW dataset, and remapping it would move the
|
|
4031
|
+
// selection off the row the raw-index contract points at.
|
|
4032
|
+
if (new Set(previous).size !== previous.length)
|
|
4033
|
+
return;
|
|
4034
|
+
for (const row of previous)
|
|
4035
|
+
if (!identities.has(row))
|
|
4036
|
+
return;
|
|
4037
|
+
const remapped = this.selectedRows
|
|
4038
|
+
.map((index) => this.indexOfRow(previous[index]))
|
|
4039
|
+
.filter((index) => index >= 0);
|
|
4040
|
+
if (remapped.length === this.selectedRows.length
|
|
4041
|
+
&& remapped.every((index, i) => index === this.selectedRows[i]))
|
|
4042
|
+
return;
|
|
4043
|
+
this.selectedRows = remapped;
|
|
4044
|
+
}
|
|
3463
4045
|
dispatchRowSelectionChanged(rowIndex, selected) {
|
|
3464
4046
|
return {
|
|
3465
4047
|
selectedRows: this.selectedRows,
|
|
@@ -3579,20 +4161,37 @@ let SniceTable = (() => {
|
|
|
3579
4161
|
const scrollContainer = this.getScrollContainer();
|
|
3580
4162
|
if (!scrollContainer)
|
|
3581
4163
|
return;
|
|
3582
|
-
this.
|
|
4164
|
+
this.syncVirtualRows();
|
|
3583
4165
|
this.virtualizer.attach({
|
|
3584
4166
|
rowHeight: (displayIndex) => this.getVirtualRowHeight(displayIndex),
|
|
3585
4167
|
bufferPx: this.virtualBuffer,
|
|
3586
4168
|
totalRows: this.virtualRowsSnapshot.length,
|
|
3587
4169
|
scrollContainer,
|
|
4170
|
+
// The host template swaps its structural branch (slotted rows ⇄ native
|
|
4171
|
+
// table) without re-attaching. Re-resolving keeps the window painting into
|
|
4172
|
+
// the LIVE tbody instead of the detached branch's orphaned one.
|
|
4173
|
+
resolveScrollContainer: () => this.getScrollContainer(),
|
|
3588
4174
|
renderRange: (start, end) => this.renderRowRange(start, end),
|
|
4175
|
+
// Spacers span the real grid. A spacer wider than the table invents
|
|
4176
|
+
// columns for fixed layout to spend the frame's spare width on.
|
|
4177
|
+
resolveColumnSpan: () => this.getTotalColumnSpan(),
|
|
3589
4178
|
// Pinned rows live outside the windowed range so they are always present,
|
|
3590
4179
|
// exactly like the non-virtual path (renderBody).
|
|
3591
4180
|
renderPinnedTop: () => this.renderPinnedRows(this.pinnedTopRows, 'top'),
|
|
3592
4181
|
renderPinnedBottom: () => this.renderPinnedRows(this.pinnedBottomRows, 'bottom'),
|
|
4182
|
+
// A zero-row window still owes the user the empty/loading/error row the
|
|
4183
|
+
// ordinary path renders.
|
|
4184
|
+
renderEmptyBody: () => this.createEmptyBodyRow(),
|
|
3593
4185
|
// Scroll-driven windows bypass renderBody; restore grid roles, logical
|
|
3594
4186
|
// row indices, and roving focus after every inserted range.
|
|
3595
|
-
afterRender: () =>
|
|
4187
|
+
afterRender: () => {
|
|
4188
|
+
this.keyboard.refresh();
|
|
4189
|
+
// Every window rebuilds the tbody from scratch, taking any filler with
|
|
4190
|
+
// it. Virtualization is not exempt from the fill-row contract, and
|
|
4191
|
+
// renderBody()'s own tail call never runs on the virtual path, so the
|
|
4192
|
+
// filler is re-evaluated here instead.
|
|
4193
|
+
this.updateFillRow();
|
|
4194
|
+
},
|
|
3596
4195
|
});
|
|
3597
4196
|
}
|
|
3598
4197
|
getVirtualRowHeight(displayIndex) {
|
|
@@ -3611,6 +4210,30 @@ let SniceTable = (() => {
|
|
|
3611
4210
|
* virtualized); otherwise it is the filtered data. Each descriptor carries
|
|
3612
4211
|
* the logical index used for `data-index`, selection, and detail lookups.
|
|
3613
4212
|
*/
|
|
4213
|
+
/**
|
|
4214
|
+
* Re-derive the windowed row model and hand it to the virtualizer.
|
|
4215
|
+
*
|
|
4216
|
+
* `virtualRowsSnapshot` is the ONLY state the paint pipeline caches, and
|
|
4217
|
+
* `renderRowRange` paints from it. Any caller that resolves a display index
|
|
4218
|
+
* against a FRESH `getVirtualRows()` and then asks the virtualizer to paint
|
|
4219
|
+
* (scrollToRow, the keyboard ensure-visible hook) must republish that model
|
|
4220
|
+
* first, or it scrolls to an offset computed in one list and paints rows from
|
|
4221
|
+
* another — the requested row never appears. Returns the fresh model so the
|
|
4222
|
+
* caller indexes exactly what will be painted.
|
|
4223
|
+
*
|
|
4224
|
+
* `paint` false leaves the repaint to the caller (scrollToIndex refreshes the
|
|
4225
|
+
* window itself). Those callers must not paint from here: the virtualizer's
|
|
4226
|
+
* afterRender hook re-runs keyboard.refresh(), whose ensure-visible callback
|
|
4227
|
+
* lands back in ensureKeyboardRowRendered — an unbounded recursion.
|
|
4228
|
+
*/
|
|
4229
|
+
syncVirtualRows(paint = true) {
|
|
4230
|
+
this.virtualRowsSnapshot = this.getVirtualRows();
|
|
4231
|
+
// Not yet attached during setupVirtualization; attach() takes the count.
|
|
4232
|
+
if (this.virtualizer.isEnabled()) {
|
|
4233
|
+
this.virtualizer.setTotalRows(this.virtualRowsSnapshot.length, paint);
|
|
4234
|
+
}
|
|
4235
|
+
return this.virtualRowsSnapshot;
|
|
4236
|
+
}
|
|
3614
4237
|
getVirtualRows() {
|
|
3615
4238
|
const filtered = this.getFilteredData();
|
|
3616
4239
|
// F: the flattened group+row+aggregate list is the virtual model when
|
|
@@ -3633,14 +4256,7 @@ let SniceTable = (() => {
|
|
|
3633
4256
|
const rows = this.virtualRowsSnapshot.length > 0
|
|
3634
4257
|
? this.virtualRowsSnapshot
|
|
3635
4258
|
: this.getVirtualRows();
|
|
3636
|
-
const
|
|
3637
|
-
(this.masterDetail.isEnabled() ? 1 : 0) +
|
|
3638
|
-
(this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
|
|
3639
|
-
const managedColumns = this.columnManager.getAllStates();
|
|
3640
|
-
const visibleColumnCount = managedColumns.length > 0
|
|
3641
|
-
? this.columnManager.getVisibleColumns().length
|
|
3642
|
-
: this.columns.length;
|
|
3643
|
-
const totalColSpan = visibleColumnCount + extraCols;
|
|
4259
|
+
const totalColSpan = this.getTotalColumnSpan();
|
|
3644
4260
|
const structuralSig = this.computeStructuralSig();
|
|
3645
4261
|
// Task B: recycle rows that stay in the window across a scroll shift. A row
|
|
3646
4262
|
// still visible after scrolling keeps its data-object key and signature, so
|
|
@@ -3686,9 +4302,10 @@ let SniceTable = (() => {
|
|
|
3686
4302
|
// Master-detail: render the expanded detail row in-window. The detail
|
|
3687
4303
|
// A fixed detailHeight participates in virtual spacer math through
|
|
3688
4304
|
// getVirtualRowHeight. Auto-height content is still rendered in-window
|
|
3689
|
-
// and contributes through normal table layout. Tree rows
|
|
3690
|
-
// detail
|
|
3691
|
-
|
|
4305
|
+
// and contributes through normal table layout. Tree rows compose with
|
|
4306
|
+
// master-detail like any other row; only generated gap nodes (index -1)
|
|
4307
|
+
// have no data row to describe.
|
|
4308
|
+
if (dataIndex >= 0 && this.masterDetail.isEnabled() && this.masterDetail.isExpanded(dataIndex)) {
|
|
3692
4309
|
const detailRow = this.masterDetail.createDetailRow(data, dataIndex, totalColSpan);
|
|
3693
4310
|
if (detailRow)
|
|
3694
4311
|
fragment.appendChild(detailRow);
|
|
@@ -3714,7 +4331,8 @@ let SniceTable = (() => {
|
|
|
3714
4331
|
// data row rather than an unrelated header/subtotal.
|
|
3715
4332
|
if (this.usesGroupingDisplayModel() || this.treeData.isEnabled()) {
|
|
3716
4333
|
const row = this.data[index];
|
|
3717
|
-
|
|
4334
|
+
// Resolve against the model the virtualizer is about to paint from.
|
|
4335
|
+
const displayIndex = this.syncVirtualRows(false).findIndex((entry) => entry.data === row);
|
|
3718
4336
|
if (displayIndex >= 0)
|
|
3719
4337
|
this.virtualizer.scrollToIndex(displayIndex);
|
|
3720
4338
|
return;
|
|
@@ -3723,6 +4341,14 @@ let SniceTable = (() => {
|
|
|
3723
4341
|
}
|
|
3724
4342
|
/** Rows currently reachable by grid keyboard navigation, in display order. */
|
|
3725
4343
|
getKeyboardColumnCount() {
|
|
4344
|
+
// List mode paints tool cells plus ONE full-width cell per row, and its
|
|
4345
|
+
// header no longer mirrors the body — count the body shape directly.
|
|
4346
|
+
if (this.usesListRowRenderer()) {
|
|
4347
|
+
return 1
|
|
4348
|
+
+ (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0)
|
|
4349
|
+
+ (this.masterDetail.isEnabled() ? 1 : 0)
|
|
4350
|
+
+ (this.hasSelectionColumn() ? 1 : 0);
|
|
4351
|
+
}
|
|
3726
4352
|
const rendered = this.shadowRoot?.querySelectorAll('thead tr.column-header-row > th').length ?? 0;
|
|
3727
4353
|
if (rendered > 0)
|
|
3728
4354
|
return rendered;
|
|
@@ -3778,7 +4404,9 @@ let SniceTable = (() => {
|
|
|
3778
4404
|
const row = this.getKeyboardRows()[rowIndex];
|
|
3779
4405
|
if (row === undefined)
|
|
3780
4406
|
return;
|
|
3781
|
-
|
|
4407
|
+
// The window is painted from `virtualRowsSnapshot`; republish it so the
|
|
4408
|
+
// offset we scroll to and the rows that land there come from one model.
|
|
4409
|
+
const displayIndex = this.syncVirtualRows(false).findIndex((entry) => entry.data === row);
|
|
3782
4410
|
if (displayIndex >= 0)
|
|
3783
4411
|
this.virtualizer.scrollToIndex(displayIndex);
|
|
3784
4412
|
}
|
|
@@ -3906,8 +4534,7 @@ let SniceTable = (() => {
|
|
|
3906
4534
|
return items;
|
|
3907
4535
|
const totalPages = Math.max(1, Math.ceil(items.length / this.pageSize));
|
|
3908
4536
|
const page = Math.max(1, Math.min(this.currentPage, totalPages));
|
|
3909
|
-
|
|
3910
|
-
this.currentPage = page;
|
|
4537
|
+
this.clampCurrentPage(page);
|
|
3911
4538
|
const start = (page - 1) * this.pageSize;
|
|
3912
4539
|
return items.slice(start, start + this.pageSize);
|
|
3913
4540
|
}
|
|
@@ -3927,8 +4554,7 @@ let SniceTable = (() => {
|
|
|
3927
4554
|
return rows;
|
|
3928
4555
|
const totalPages = Math.max(1, Math.ceil(rows.length / this.pageSize));
|
|
3929
4556
|
const page = Math.max(1, Math.min(this.currentPage, totalPages));
|
|
3930
|
-
|
|
3931
|
-
this.currentPage = page;
|
|
4557
|
+
this.clampCurrentPage(page);
|
|
3932
4558
|
const start = (page - 1) * this.pageSize;
|
|
3933
4559
|
return rows.slice(start, start + this.pageSize);
|
|
3934
4560
|
}
|
|
@@ -3990,16 +4616,21 @@ let SniceTable = (() => {
|
|
|
3990
4616
|
this.renderBody();
|
|
3991
4617
|
this.dispatchColumnPinChange(key, false);
|
|
3992
4618
|
}
|
|
4619
|
+
// A measured width is the column's width, not the header's: the body has to
|
|
4620
|
+
// be repainted with it too, or the th and its tds carry different numbers
|
|
4621
|
+
// and the next body-only re-render paints the stale one.
|
|
3993
4622
|
autoSizeColumn(key) {
|
|
3994
4623
|
if (this.tbody) {
|
|
3995
4624
|
this.columnManager.autoSizeColumn(key, this.tbody);
|
|
3996
4625
|
this.renderHeader();
|
|
4626
|
+
this.renderBody();
|
|
3997
4627
|
}
|
|
3998
4628
|
}
|
|
3999
4629
|
autoSizeAllColumns() {
|
|
4000
4630
|
if (this.tbody) {
|
|
4001
4631
|
this.columnManager.autoSizeAll(this.tbody);
|
|
4002
4632
|
this.renderHeader();
|
|
4633
|
+
this.renderBody();
|
|
4003
4634
|
}
|
|
4004
4635
|
}
|
|
4005
4636
|
moveColumn(key, toIndex) {
|
|
@@ -4259,8 +4890,7 @@ let SniceTable = (() => {
|
|
|
4259
4890
|
this.masterDetail.onHeightChange = () => {
|
|
4260
4891
|
if (!this.virtualize || !this.virtualizer.isEnabled())
|
|
4261
4892
|
return;
|
|
4262
|
-
this.
|
|
4263
|
-
this.virtualizer.setTotalRows(this.virtualRowsSnapshot.length);
|
|
4893
|
+
this.syncVirtualRows();
|
|
4264
4894
|
};
|
|
4265
4895
|
this.masterDetail.prepare(this.data);
|
|
4266
4896
|
this.renderHeader();
|
|
@@ -4487,6 +5117,9 @@ let SniceTable = (() => {
|
|
|
4487
5117
|
// The row recycler cannot infer that a callback identity changed from
|
|
4488
5118
|
// row data alone. Force fresh rows so list markup replaces table cells.
|
|
4489
5119
|
this.renderedRows = new Map();
|
|
5120
|
+
// Installing a renderer removes the column geometry the header describes;
|
|
5121
|
+
// clearing it back to null restores columnar rows AND their header.
|
|
5122
|
+
this.renderHeader();
|
|
4490
5123
|
this.renderBody();
|
|
4491
5124
|
}
|
|
4492
5125
|
setListViewRenderer(fn) {
|
|
@@ -4505,7 +5138,7 @@ let SniceTable = (() => {
|
|
|
4505
5138
|
tr.style.height = `${this.rowHeight}px`;
|
|
4506
5139
|
}
|
|
4507
5140
|
const isSelected = this.selectedRows.includes(index);
|
|
4508
|
-
|
|
5141
|
+
this.stampRowSelection(tr, isSelected);
|
|
4509
5142
|
// Row DnD handle
|
|
4510
5143
|
if (this.rowReorder && this.rowDnD.isEnabled()) {
|
|
4511
5144
|
tr.appendChild(this.rowDnD.createDragHandle());
|
|
@@ -4576,7 +5209,18 @@ let SniceTable = (() => {
|
|
|
4576
5209
|
const toggle = this.treeData.createToggle(treeRow);
|
|
4577
5210
|
td.appendChild(toggle);
|
|
4578
5211
|
const textSpan = document.createElement('span');
|
|
4579
|
-
textSpan.
|
|
5212
|
+
textSpan.className = 'tree-label';
|
|
5213
|
+
// The group cell keeps its plain label next to the indent/chevron, but
|
|
5214
|
+
// it is still a cell: renderCell owns it outright, and otherwise the
|
|
5215
|
+
// documented display pipeline (formatter, then valueFormatter) decides
|
|
5216
|
+
// the text. Only a column with neither shows the raw working value.
|
|
5217
|
+
if (typeof column.renderCell === 'function') {
|
|
5218
|
+
textSpan.appendChild(this.createCellElement(column, value, rowData));
|
|
5219
|
+
}
|
|
5220
|
+
else {
|
|
5221
|
+
textSpan.textContent = this.cellDisplayText(column, value, rowData)
|
|
5222
|
+
?? (value == null ? '' : String(value));
|
|
5223
|
+
}
|
|
4580
5224
|
// Clicking the text label also toggles expand/collapse
|
|
4581
5225
|
if (treeRow.hasChildren) {
|
|
4582
5226
|
textSpan.style.cursor = 'pointer';
|
|
@@ -4595,21 +5239,7 @@ let SniceTable = (() => {
|
|
|
4595
5239
|
const state = this.columnManager.getState(column.key);
|
|
4596
5240
|
if (state) {
|
|
4597
5241
|
td.style.width = `${state.width}px`;
|
|
4598
|
-
|
|
4599
|
-
if (state.pinned === 'left') {
|
|
4600
|
-
const offsets = this.columnManager.getPinnedLeftOffsets();
|
|
4601
|
-
td.classList.add('pinned-cell');
|
|
4602
|
-
td.style.position = 'sticky';
|
|
4603
|
-
td.style.left = `${offsets.get(column.key) ?? 0}px`;
|
|
4604
|
-
td.style.zIndex = '1';
|
|
4605
|
-
}
|
|
4606
|
-
else if (state.pinned === 'right') {
|
|
4607
|
-
const offsets = this.columnManager.getPinnedRightOffsets();
|
|
4608
|
-
td.classList.add('pinned-cell');
|
|
4609
|
-
td.style.position = 'sticky';
|
|
4610
|
-
td.style.right = `${offsets.get(column.key) ?? 0}px`;
|
|
4611
|
-
td.style.zIndex = '1';
|
|
4612
|
-
}
|
|
5242
|
+
this.applyPinnedCell(td, column.key, state, '1');
|
|
4613
5243
|
}
|
|
4614
5244
|
tr.appendChild(td);
|
|
4615
5245
|
});
|
|
@@ -4773,20 +5403,7 @@ let SniceTable = (() => {
|
|
|
4773
5403
|
const state = this.columnManager.getState(column.key);
|
|
4774
5404
|
if (state) {
|
|
4775
5405
|
td.style.width = `${state.width}px`;
|
|
4776
|
-
|
|
4777
|
-
const offsets = this.columnManager.getPinnedLeftOffsets();
|
|
4778
|
-
td.classList.add('pinned-cell');
|
|
4779
|
-
td.style.position = 'sticky';
|
|
4780
|
-
td.style.left = `${offsets.get(column.key) ?? 0}px`;
|
|
4781
|
-
td.style.zIndex = '1';
|
|
4782
|
-
}
|
|
4783
|
-
else if (state.pinned === 'right') {
|
|
4784
|
-
const offsets = this.columnManager.getPinnedRightOffsets();
|
|
4785
|
-
td.classList.add('pinned-cell');
|
|
4786
|
-
td.style.position = 'sticky';
|
|
4787
|
-
td.style.right = `${offsets.get(column.key) ?? 0}px`;
|
|
4788
|
-
td.style.zIndex = '1';
|
|
4789
|
-
}
|
|
5406
|
+
this.applyPinnedCell(td, column.key, state, '1');
|
|
4790
5407
|
}
|
|
4791
5408
|
tr.appendChild(td);
|
|
4792
5409
|
});
|
|
@@ -4967,6 +5584,26 @@ let SniceTable = (() => {
|
|
|
4967
5584
|
// C1: reactive, no attribute reflection. A post-mount assignment (or the
|
|
4968
5585
|
// setData/setColumns aliases) re-renders through the microtask-coalesced
|
|
4969
5586
|
// queue — see handleColumnsAssignment / handleDataAssignment.
|
|
5587
|
+
//
|
|
5588
|
+
// `hasChanged: () => true` — ASSIGNMENT IS THE SIGNAL, NOT IDENTITY.
|
|
5589
|
+
// The core dirty-check (notEqual in packages/core/src/utils.ts, applied by the
|
|
5590
|
+
// @property setter in element.ts) is identity-based and stays that way for
|
|
5591
|
+
// every other property: that is the right default for reactivity in general.
|
|
5592
|
+
// But `data` and `columns` are bulk payloads that callers routinely mutate in
|
|
5593
|
+
// place and then publish by re-assigning the SAME array reference
|
|
5594
|
+
// (`rows.push(row); table.data = rows`). The documented contract is that
|
|
5595
|
+
// assigning `data`/`columns` rerenders, so these two properties opt OUT of the
|
|
5596
|
+
// identity gate locally rather than weakening it globally.
|
|
5597
|
+
//
|
|
5598
|
+
// Tradeoff — double-render cost: a genuinely redundant assignment
|
|
5599
|
+
// (`table.data = table.data` with nothing changed) now costs a render that the
|
|
5600
|
+
// identity gate used to swallow, and a caller who assigns both `columns` and
|
|
5601
|
+
// `data` pays two schedule calls instead of one. It is bounded: both watchers
|
|
5602
|
+
// go through scheduleRender(), which coalesces a burst within a tick into a
|
|
5603
|
+
// single microtask flush, so the worst case is one extra body paint per tick,
|
|
5604
|
+
// never per assignment. Deep-equality diffing was rejected as the alternative
|
|
5605
|
+
// — it is O(rows x columns) on every assignment, i.e. a cost paid by the
|
|
5606
|
+
// common (genuinely-changed) path to save the rare redundant one.
|
|
4970
5607
|
this.columns = (__runInitializers(this, _searchDebounce_extraInitializers), __runInitializers(this, _columns_initializers, []));
|
|
4971
5608
|
this.data = (__runInitializers(this, _columns_extraInitializers), __runInitializers(this, _data_initializers, []));
|
|
4972
5609
|
/**
|
|
@@ -4987,7 +5624,8 @@ let SniceTable = (() => {
|
|
|
4987
5624
|
this.selector = __runInitializers(this, _selector_initializers, '');
|
|
4988
5625
|
this.selectorOptions = (__runInitializers(this, _selector_extraInitializers), __runInitializers(this, _selectorOptions_initializers, []));
|
|
4989
5626
|
this.loading = (__runInitializers(this, _selectorOptions_extraInitializers), __runInitializers(this, _loading_initializers, false));
|
|
4990
|
-
// Set when a remote-mode load fails; cleared
|
|
5627
|
+
// Set when a remote-mode load fails; cleared by clearLoadError() as soon as
|
|
5628
|
+
// ANY fresh dataset arrives (see there).
|
|
4991
5629
|
// Not a @property — renderBody() reads it directly, no attribute needed.
|
|
4992
5630
|
this.loadError = (__runInitializers(this, _loading_extraInitializers), null);
|
|
4993
5631
|
this.virtualize = __runInitializers(this, _virtualize_initializers, false);
|
|
@@ -4997,7 +5635,16 @@ let SniceTable = (() => {
|
|
|
4997
5635
|
this.editable = (__runInitializers(this, _columnResize_extraInitializers), __runInitializers(this, _editable_initializers, false));
|
|
4998
5636
|
this.editMode = (__runInitializers(this, _editable_extraInitializers), __runInitializers(this, _editMode_initializers, 'cell'));
|
|
4999
5637
|
this.density = (__runInitializers(this, _editMode_extraInitializers), __runInitializers(this, _density_initializers, 'standard'));
|
|
5000
|
-
|
|
5638
|
+
/**
|
|
5639
|
+
* How the columns relate to the frame's width.
|
|
5640
|
+
*
|
|
5641
|
+
* `scroll` keeps every column at or above its `minWidth` and lets the frame
|
|
5642
|
+
* scroll horizontally when they no longer fit. `squish` makes the frame the
|
|
5643
|
+
* hard constraint instead: minimums relax, content ellipsises, and the table
|
|
5644
|
+
* never scrolls sideways. See the `ColumnFit` docs in snice-table.types.ts.
|
|
5645
|
+
*/
|
|
5646
|
+
this.columnFit = (__runInitializers(this, _density_extraInitializers), __runInitializers(this, _columnFit_initializers, 'scroll'));
|
|
5647
|
+
this.headerFilters = (__runInitializers(this, _columnFit_extraInitializers), __runInitializers(this, _headerFilters_initializers, false));
|
|
5001
5648
|
this.quickFilter = (__runInitializers(this, _headerFilters_extraInitializers), __runInitializers(this, _quickFilter_initializers, false));
|
|
5002
5649
|
this.rowReorder = (__runInitializers(this, _quickFilter_extraInitializers), __runInitializers(this, _rowReorder_initializers, false));
|
|
5003
5650
|
this.columnReorder = (__runInitializers(this, _rowReorder_extraInitializers), __runInitializers(this, _columnReorder_initializers, false));
|
|
@@ -5049,7 +5696,9 @@ let SniceTable = (() => {
|
|
|
5049
5696
|
this.table = __runInitializers(this, _table_initializers, void 0);
|
|
5050
5697
|
this.thead = (__runInitializers(this, _table_extraInitializers), __runInitializers(this, _thead_initializers, void 0));
|
|
5051
5698
|
this.tbody = (__runInitializers(this, _thead_extraInitializers), __runInitializers(this, _tbody_initializers, void 0));
|
|
5052
|
-
|
|
5699
|
+
/** Frame width the columns were last fitted to — see handleFrameResize. */
|
|
5700
|
+
this.lastFittedFrameWidth = (__runInitializers(this, _tbody_extraInitializers), 0);
|
|
5701
|
+
this.unsortedData = [];
|
|
5053
5702
|
this.dataRequestTimeout = null;
|
|
5054
5703
|
// A1: row-object → its index in `this.data`. Replaces the O(n) `data.indexOf`
|
|
5055
5704
|
// that ran per row on every selection toggle (was O(n²) for select-all).
|
|
@@ -5081,6 +5730,10 @@ let SniceTable = (() => {
|
|
|
5081
5730
|
// C1: set while the imperative setData() runs so the data @watch resyncs the
|
|
5082
5731
|
// model but skips the auto-render (the caller drives the body fill).
|
|
5083
5732
|
this.settingDataImperative = false;
|
|
5733
|
+
// Set while the row-reorder handler re-publishes `this.data` as a new array
|
|
5734
|
+
// of the SAME rows. Tells the data @watch that no fresh dataset arrived, so a
|
|
5735
|
+
// pending load error must survive the drag.
|
|
5736
|
+
this.reorderingRows = false;
|
|
5084
5737
|
// C1/C2: microtask-coalesced render queue. Reactive assignments schedule a
|
|
5085
5738
|
// render here instead of calling renderHeader/renderBody synchronously —
|
|
5086
5739
|
// MANDATORY, because happy-dom crashes constructing cell elements inside a
|
|
@@ -5088,6 +5741,32 @@ let SniceTable = (() => {
|
|
|
5088
5741
|
this.renderQueued = false;
|
|
5089
5742
|
this.pendingHeaderRender = false;
|
|
5090
5743
|
this.pendingBodyRender = false;
|
|
5744
|
+
// Re-arm bookkeeping for a body render that found no <tbody> to paint into.
|
|
5745
|
+
// Bounded: the slotted-rows template legitimately has no <tbody>, and an
|
|
5746
|
+
// unbounded retry there would spin every frame.
|
|
5747
|
+
this.bodyRenderRetries = 0;
|
|
5748
|
+
this.bodyRenderRetryQueued = false;
|
|
5749
|
+
/**
|
|
5750
|
+
* Write a page number the display model just derived (a re-clamp after the
|
|
5751
|
+
* row set shortened), WITHOUT re-entering the render scheduler.
|
|
5752
|
+
*
|
|
5753
|
+
* The three clamp sites all sit inside a display-model computation whose
|
|
5754
|
+
* caller paints immediately afterwards with the clamped page — the ordinary
|
|
5755
|
+
* body path slices with it and then renders the pager, the grouping and tree
|
|
5756
|
+
* models return the clamped slice. Letting the @watch fire here queued a
|
|
5757
|
+
* second, byte-identical body pass per delivery — the measured cost, and what
|
|
5758
|
+
* the regression test pins. Suppressing the write's watcher makes the clamp
|
|
5759
|
+
* cost zero extra passes and makes an oscillating clamp structurally unable
|
|
5760
|
+
* to loop.
|
|
5761
|
+
*
|
|
5762
|
+
* All three sites are reachable only under `paginationMode === 'client'`, so
|
|
5763
|
+
* the watcher's server branch (debouncedDataRequest, whose `loading = true`
|
|
5764
|
+
* re-enters renderBody SYNCHRONOUSLY through the loading watcher — from the
|
|
5765
|
+
* middle of a render) is not reachable from a clamp today. It is the reason
|
|
5766
|
+
* to keep this guard rather than to drop the watcher suppression if a fourth
|
|
5767
|
+
* clamp site ever lands outside that guard.
|
|
5768
|
+
*/
|
|
5769
|
+
this.clampingPage = false;
|
|
5091
5770
|
/**
|
|
5092
5771
|
* A structural fingerprint of everything `createRow` reads that is NOT
|
|
5093
5772
|
* per-row data: visible columns + their widths/pinning/offsets, tool columns,
|