@ornery/ui-grid-vanilla 0.1.10 → 1.0.1
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/AGENTS.md +70 -0
- package/README.md +196 -0
- package/demo/main.ts +0 -1
- package/dist/index.d.mts +582 -55
- package/dist/index.d.ts +582 -55
- package/dist/index.js +4959 -1429
- package/dist/index.mjs +5137 -1560
- package/package.json +11 -8
- package/scss-plugin.ts +65 -0
- package/src/adopt-styles.ts +39 -0
- package/src/attribute-bridge.ts +291 -0
- package/src/auto-height.test.ts +142 -0
- package/src/auto-resize.test.ts +207 -0
- package/src/cell-nav.test.ts +262 -0
- package/src/components/grid-body-cell/grid-body-cell.ts +23 -0
- package/src/components/grid-body-cell/index.ts +1 -0
- package/src/components/grid-cell-editor/grid-cell-editor.ts +104 -0
- package/src/components/grid-cell-editor/index.ts +1 -0
- package/src/components/grid-filter-cell/grid-filter-cell.scss +14 -0
- package/src/components/grid-filter-cell/grid-filter-cell.template.ts +20 -0
- package/src/components/grid-filter-cell/grid-filter-cell.ts +121 -0
- package/src/components/grid-filter-cell/index.ts +1 -0
- package/src/components/grid-group-row/grid-group-row.scss +24 -0
- package/src/components/grid-group-row/grid-group-row.template.ts +23 -0
- package/src/components/grid-group-row/grid-group-row.ts +94 -0
- package/src/components/grid-group-row/index.ts +1 -0
- package/src/components/grid-header-cell/grid-header-cell.ts +17 -0
- package/src/components/grid-header-cell/index.ts +1 -0
- package/src/components/grid-pagination/grid-pagination.scss +39 -0
- package/src/components/grid-pagination/grid-pagination.template.ts +40 -0
- package/src/components/grid-pagination/grid-pagination.ts +204 -0
- package/src/components/grid-pagination/index.ts +1 -0
- package/src/components/grid-template/grid-template.ts +166 -0
- package/src/components/grid-template/index.ts +1 -0
- package/src/events.ts +824 -0
- package/src/exporter.test.ts +186 -0
- package/src/focus.ts +379 -0
- package/src/framework-slots.ts +440 -0
- package/src/grid-controller.ts +1532 -21
- package/src/icons.ts +88 -0
- package/src/importer.test.ts +147 -0
- package/src/index.integration.test.ts +2 -2
- package/src/index.ts +9 -0
- package/src/infinite-scroll.test.ts +220 -0
- package/src/keyboard-navigation.test.ts +579 -0
- package/src/patch.ts +722 -0
- package/src/render.ts +465 -0
- package/src/row-edit.test.ts +191 -0
- package/src/row-selection.test.ts +550 -0
- package/src/save-state.test.ts +235 -0
- package/src/scss.d.ts +4 -0
- package/src/templates/index.ts +62 -0
- package/src/test-setup.ts +23 -0
- package/src/ui-grid-empty.template.ts +23 -0
- package/src/ui-grid-shell.scss +25 -0
- package/src/ui-grid-shell.template.ts +45 -0
- package/src/ui-grid-standalone.element.ts +751 -1112
- package/src/utils/cell-class.ts +70 -0
- package/src/utils/display-items.ts +15 -0
- package/src/utils/dom.ts +64 -0
- package/src/validate.test.ts +170 -0
- package/tsup.config.ts +10 -0
- package/vite.config.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornery/ui-grid-vanilla",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Vanilla DOM wrapper for the Rust/WASM-backed @ornery/ui-grid-core custom element",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -16,18 +16,21 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@ornery/ui-grid-core": "0.
|
|
19
|
+
"@ornery/ui-grid-core": "1.0.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"
|
|
22
|
+
"happy-dom": "^20.9.0",
|
|
23
|
+
"jsdom": "^26.0.0",
|
|
24
|
+
"sass": "^1.77.0",
|
|
23
25
|
"tsup": "^8.0.0",
|
|
26
|
+
"typescript": "~5.8.0",
|
|
24
27
|
"vite": "^6.0.0",
|
|
25
|
-
"vitest": "^4.1.0"
|
|
26
|
-
"jsdom": "^26.0.0"
|
|
28
|
+
"vitest": "^4.1.0"
|
|
27
29
|
},
|
|
28
30
|
"scripts": {
|
|
29
|
-
"start": "vite serve",
|
|
30
|
-
"build": "npm run build --prefix ../ui-grid-core && tsup
|
|
31
|
-
"
|
|
31
|
+
"start": "SASS_PATH=node_modules vite serve",
|
|
32
|
+
"build": "npm run build --prefix ../ui-grid-core && SASS_PATH=node_modules tsup",
|
|
33
|
+
"watch": "npm run build --prefix ../ui-grid-core && SASS_PATH=node_modules tsup --watch",
|
|
34
|
+
"test": "SASS_PATH=node_modules vitest run"
|
|
32
35
|
}
|
|
33
36
|
}
|
package/scss-plugin.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as sass from 'sass';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { Plugin } from 'vite';
|
|
4
|
+
import type { Plugin as EsbuildPlugin } from 'esbuild';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
const SCSS_SUFFIX = '?inline-css';
|
|
8
|
+
|
|
9
|
+
export function scssInlinePlugin(): Plugin {
|
|
10
|
+
return {
|
|
11
|
+
name: 'scss-inline',
|
|
12
|
+
enforce: 'pre',
|
|
13
|
+
resolveId(source, importer) {
|
|
14
|
+
if (source.endsWith('.scss') && importer && !source.includes('node_modules')) {
|
|
15
|
+
const resolved = path.resolve(path.dirname(importer), source);
|
|
16
|
+
return resolved + SCSS_SUFFIX;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
load(id) {
|
|
21
|
+
if (!id.endsWith(SCSS_SUFFIX)) return null;
|
|
22
|
+
const filePath = id.slice(0, -SCSS_SUFFIX.length);
|
|
23
|
+
const result = sass.compile(filePath, {
|
|
24
|
+
style: 'compressed',
|
|
25
|
+
loadPaths: [
|
|
26
|
+
path.resolve(process.cwd(), 'node_modules'),
|
|
27
|
+
path.resolve(process.cwd(), '../../node_modules'),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
return `export default ${JSON.stringify(result.css)};`;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function scssInlineEsbuild(): EsbuildPlugin {
|
|
36
|
+
return {
|
|
37
|
+
name: 'scss-inline',
|
|
38
|
+
setup(build) {
|
|
39
|
+
build.onResolve({ filter: /\.scss$/ }, (args) => {
|
|
40
|
+
if (args.resolveDir && !args.path.includes('node_modules')) {
|
|
41
|
+
return {
|
|
42
|
+
path: path.resolve(args.resolveDir, args.path),
|
|
43
|
+
namespace: 'scss-inline',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
build.onLoad({ filter: /.*/, namespace: 'scss-inline' }, (args) => {
|
|
50
|
+
const result = sass.compile(args.path, {
|
|
51
|
+
style: 'compressed',
|
|
52
|
+
loadPaths: [
|
|
53
|
+
path.resolve(process.cwd(), 'node_modules'),
|
|
54
|
+
path.resolve(process.cwd(), '..'),
|
|
55
|
+
path.resolve(process.cwd(), '../../node_modules'),
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
contents: `export default ${JSON.stringify(result.css)};`,
|
|
60
|
+
loader: 'js',
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
let sheetsSupported: boolean | null = null;
|
|
2
|
+
|
|
3
|
+
function supportsAdoptedStyleSheets(): boolean {
|
|
4
|
+
if (sheetsSupported === null) {
|
|
5
|
+
try {
|
|
6
|
+
const s = new CSSStyleSheet();
|
|
7
|
+
s.replaceSync('');
|
|
8
|
+
sheetsSupported = true;
|
|
9
|
+
} catch {
|
|
10
|
+
sheetsSupported = false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return sheetsSupported;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sheetCache = new WeakMap<object, CSSStyleSheet>();
|
|
17
|
+
|
|
18
|
+
export function adoptStyles(shadowRoot: ShadowRoot, css: string, key: object): void {
|
|
19
|
+
if (!supportsAdoptedStyleSheets()) {
|
|
20
|
+
let styleEl = shadowRoot.querySelector('style[data-grid-styles]') as HTMLStyleElement | null;
|
|
21
|
+
if (!styleEl) {
|
|
22
|
+
styleEl = document.createElement('style');
|
|
23
|
+
styleEl.setAttribute('data-grid-styles', '');
|
|
24
|
+
styleEl.textContent = css;
|
|
25
|
+
shadowRoot.prepend(styleEl);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let sheet = sheetCache.get(key);
|
|
31
|
+
if (!sheet) {
|
|
32
|
+
sheet = new CSSStyleSheet();
|
|
33
|
+
sheet.replaceSync(css);
|
|
34
|
+
sheetCache.set(key, sheet);
|
|
35
|
+
}
|
|
36
|
+
if (!shadowRoot.adoptedStyleSheets.includes(sheet)) {
|
|
37
|
+
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sheet];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative attribute bridge.
|
|
3
|
+
*
|
|
4
|
+
* The grid element accepts its full `GridOptions` shape through HTML
|
|
5
|
+
* attributes so it can be driven declaratively from any framework (or
|
|
6
|
+
* plain HTML). This module owns:
|
|
7
|
+
*
|
|
8
|
+
* - `OBSERVED_GRID_ATTRIBUTES`: the list passed to `observedAttributes`
|
|
9
|
+
* - `parseGridAttributeOptions(el)`: reads attributes off an element and
|
|
10
|
+
* returns the `Partial<GridOptions>` they describe
|
|
11
|
+
* - the individual attribute parsers (boolean / tri-state / number / JSON)
|
|
12
|
+
*
|
|
13
|
+
* The element keeps its own `attributeOptions` cache and `buildEffectiveOptions`
|
|
14
|
+
* merge so JS-set properties can override attribute values, but the parsing
|
|
15
|
+
* itself lives here.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { GridColumnDef, GridOptions, GridRecord } from '@ornery/ui-grid-core';
|
|
19
|
+
|
|
20
|
+
/** The list passed to `static observedAttributes`. Every kebab-case name
|
|
21
|
+
* that corresponds to a GridOptions field lives here. */
|
|
22
|
+
export const OBSERVED_GRID_ATTRIBUTES: readonly string[] = [
|
|
23
|
+
// Scalar attributes
|
|
24
|
+
'grid-id',
|
|
25
|
+
'title',
|
|
26
|
+
'row-height',
|
|
27
|
+
'header-row-height',
|
|
28
|
+
'pagination-page-size',
|
|
29
|
+
'pagination-current-page',
|
|
30
|
+
'total-items',
|
|
31
|
+
'virtualization-threshold',
|
|
32
|
+
'tree-children-field',
|
|
33
|
+
'tree-indent',
|
|
34
|
+
'expandable-row-height',
|
|
35
|
+
'expandable-row-header-width',
|
|
36
|
+
'empty-message',
|
|
37
|
+
'infinite-scroll-rows-from-end',
|
|
38
|
+
// JSON attributes
|
|
39
|
+
'column-defs',
|
|
40
|
+
'data',
|
|
41
|
+
'grouping',
|
|
42
|
+
'pagination-page-sizes',
|
|
43
|
+
// Boolean flags
|
|
44
|
+
'enable-sorting',
|
|
45
|
+
'enable-filtering',
|
|
46
|
+
'enable-grouping',
|
|
47
|
+
'enable-pinning',
|
|
48
|
+
'enable-column-moving',
|
|
49
|
+
'enable-column-resizing',
|
|
50
|
+
'enable-cell-edit',
|
|
51
|
+
'enable-cell-edit-on-focus',
|
|
52
|
+
'enable-pagination',
|
|
53
|
+
'enable-pagination-controls',
|
|
54
|
+
'use-external-pagination',
|
|
55
|
+
'enable-expandable',
|
|
56
|
+
'enable-tree-view',
|
|
57
|
+
'show-tree-expand-no-children',
|
|
58
|
+
'tree-row-header-always-visible',
|
|
59
|
+
'enable-auto-resize',
|
|
60
|
+
'enable-virtualization',
|
|
61
|
+
'enable-infinite-scroll',
|
|
62
|
+
'infinite-scroll-up',
|
|
63
|
+
'infinite-scroll-down',
|
|
64
|
+
// Selection — ported from ui.grid.selection options.
|
|
65
|
+
'enable-row-selection',
|
|
66
|
+
'multi-select',
|
|
67
|
+
'no-unselect',
|
|
68
|
+
'modifier-keys-to-multi-select',
|
|
69
|
+
'enable-row-header-selection',
|
|
70
|
+
'enable-full-row-selection',
|
|
71
|
+
'enable-focus-row-on-row-header-click',
|
|
72
|
+
'enable-select-row-on-focus',
|
|
73
|
+
'enable-select-all',
|
|
74
|
+
'enable-selection-batch-event',
|
|
75
|
+
'enable-footer-total-selected',
|
|
76
|
+
'selection-row-header-width',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read every supported attribute off the element and produce the
|
|
81
|
+
* corresponding `Partial<GridOptions>`. Missing attributes are simply
|
|
82
|
+
* absent from the result — the element merges this with its imperative
|
|
83
|
+
* `options` setter so JS-set properties win.
|
|
84
|
+
*/
|
|
85
|
+
export function parseGridAttributeOptions(el: Element): Partial<GridOptions> {
|
|
86
|
+
const out: Partial<GridOptions> = {};
|
|
87
|
+
|
|
88
|
+
// Scalar string attributes
|
|
89
|
+
const gridId = el.getAttribute('grid-id');
|
|
90
|
+
if (gridId !== null) out.id = gridId;
|
|
91
|
+
|
|
92
|
+
const title = el.getAttribute('title');
|
|
93
|
+
if (title !== null) out.title = title;
|
|
94
|
+
|
|
95
|
+
const emptyMessage = el.getAttribute('empty-message');
|
|
96
|
+
if (emptyMessage !== null) out.emptyMessage = emptyMessage;
|
|
97
|
+
|
|
98
|
+
const treeChildrenField = el.getAttribute('tree-children-field');
|
|
99
|
+
if (treeChildrenField !== null) out.treeChildrenField = treeChildrenField;
|
|
100
|
+
|
|
101
|
+
// Scalar number attributes
|
|
102
|
+
const rowHeight = parseNumber(el, 'row-height');
|
|
103
|
+
if (rowHeight !== undefined) out.rowHeight = rowHeight;
|
|
104
|
+
|
|
105
|
+
const headerRowHeight = parseNumber(el, 'header-row-height');
|
|
106
|
+
if (headerRowHeight !== undefined) out.headerRowHeight = headerRowHeight;
|
|
107
|
+
|
|
108
|
+
const paginationPageSize = parseNumber(el, 'pagination-page-size');
|
|
109
|
+
if (paginationPageSize !== undefined) out.paginationPageSize = paginationPageSize;
|
|
110
|
+
|
|
111
|
+
const paginationCurrentPage = parseNumber(el, 'pagination-current-page');
|
|
112
|
+
if (paginationCurrentPage !== undefined) out.paginationCurrentPage = paginationCurrentPage;
|
|
113
|
+
|
|
114
|
+
const totalItems = parseNumber(el, 'total-items');
|
|
115
|
+
if (totalItems !== undefined) out.totalItems = totalItems;
|
|
116
|
+
|
|
117
|
+
const virtualizationThreshold = parseNumber(el, 'virtualization-threshold');
|
|
118
|
+
if (virtualizationThreshold !== undefined) out.virtualizationThreshold = virtualizationThreshold;
|
|
119
|
+
|
|
120
|
+
const treeIndent = parseNumber(el, 'tree-indent');
|
|
121
|
+
if (treeIndent !== undefined) out.treeIndent = treeIndent;
|
|
122
|
+
|
|
123
|
+
const expandableRowHeight = parseNumber(el, 'expandable-row-height');
|
|
124
|
+
if (expandableRowHeight !== undefined) out.expandableRowHeight = expandableRowHeight;
|
|
125
|
+
|
|
126
|
+
const expandableRowHeaderWidth = parseNumber(el, 'expandable-row-header-width');
|
|
127
|
+
if (expandableRowHeaderWidth !== undefined) out.expandableRowHeaderWidth = expandableRowHeaderWidth;
|
|
128
|
+
|
|
129
|
+
const infiniteScrollRowsFromEnd = parseNumber(el, 'infinite-scroll-rows-from-end');
|
|
130
|
+
if (infiniteScrollRowsFromEnd !== undefined) out.infiniteScrollRowsFromEnd = infiniteScrollRowsFromEnd;
|
|
131
|
+
|
|
132
|
+
// Boolean attributes
|
|
133
|
+
const enableSorting = parseBoolean(el, 'enable-sorting');
|
|
134
|
+
if (enableSorting !== undefined) out.enableSorting = enableSorting;
|
|
135
|
+
|
|
136
|
+
const enableFiltering = parseBoolean(el, 'enable-filtering');
|
|
137
|
+
if (enableFiltering !== undefined) out.enableFiltering = enableFiltering;
|
|
138
|
+
|
|
139
|
+
const enableGrouping = parseBoolean(el, 'enable-grouping');
|
|
140
|
+
if (enableGrouping !== undefined) out.enableGrouping = enableGrouping;
|
|
141
|
+
|
|
142
|
+
const enablePinning = parseBoolean(el, 'enable-pinning');
|
|
143
|
+
if (enablePinning !== undefined) out.enablePinning = enablePinning;
|
|
144
|
+
|
|
145
|
+
const enableColumnMoving = parseBoolean(el, 'enable-column-moving');
|
|
146
|
+
if (enableColumnMoving !== undefined) out.enableColumnMoving = enableColumnMoving;
|
|
147
|
+
|
|
148
|
+
const enableColumnResizing = parseBoolean(el, 'enable-column-resizing');
|
|
149
|
+
if (enableColumnResizing !== undefined) out.enableColumnResizing = enableColumnResizing;
|
|
150
|
+
|
|
151
|
+
const enableCellEdit = parseBoolean(el, 'enable-cell-edit');
|
|
152
|
+
if (enableCellEdit !== undefined) out.enableCellEdit = enableCellEdit;
|
|
153
|
+
|
|
154
|
+
const enableCellEditOnFocus = parseBoolean(el, 'enable-cell-edit-on-focus');
|
|
155
|
+
if (enableCellEditOnFocus !== undefined) out.enableCellEditOnFocus = enableCellEditOnFocus;
|
|
156
|
+
|
|
157
|
+
const enablePagination = parseBoolean(el, 'enable-pagination');
|
|
158
|
+
if (enablePagination !== undefined) out.enablePagination = enablePagination;
|
|
159
|
+
|
|
160
|
+
const enablePaginationControls = parseBoolean(el, 'enable-pagination-controls');
|
|
161
|
+
if (enablePaginationControls !== undefined) out.enablePaginationControls = enablePaginationControls;
|
|
162
|
+
|
|
163
|
+
const useExternalPagination = parseBoolean(el, 'use-external-pagination');
|
|
164
|
+
if (useExternalPagination !== undefined) out.useExternalPagination = useExternalPagination;
|
|
165
|
+
|
|
166
|
+
const enableExpandable = parseBoolean(el, 'enable-expandable');
|
|
167
|
+
if (enableExpandable !== undefined) out.enableExpandable = enableExpandable;
|
|
168
|
+
|
|
169
|
+
const enableTreeView = parseBoolean(el, 'enable-tree-view');
|
|
170
|
+
if (enableTreeView !== undefined) out.enableTreeView = enableTreeView;
|
|
171
|
+
|
|
172
|
+
const showTreeExpandNoChildren = parseBoolean(el, 'show-tree-expand-no-children');
|
|
173
|
+
if (showTreeExpandNoChildren !== undefined) out.showTreeExpandNoChildren = showTreeExpandNoChildren;
|
|
174
|
+
|
|
175
|
+
const treeRowHeaderAlwaysVisible = parseBoolean(el, 'tree-row-header-always-visible');
|
|
176
|
+
if (treeRowHeaderAlwaysVisible !== undefined) out.treeRowHeaderAlwaysVisible = treeRowHeaderAlwaysVisible;
|
|
177
|
+
|
|
178
|
+
const enableAutoResize = parseBoolean(el, 'enable-auto-resize');
|
|
179
|
+
if (enableAutoResize !== undefined) out.enableAutoResize = enableAutoResize;
|
|
180
|
+
|
|
181
|
+
const enableVirtualization = parseBoolean(el, 'enable-virtualization');
|
|
182
|
+
if (enableVirtualization !== undefined) out.enableVirtualization = enableVirtualization;
|
|
183
|
+
|
|
184
|
+
const enableInfiniteScroll = parseTriStateBoolean(el, 'enable-infinite-scroll');
|
|
185
|
+
if (enableInfiniteScroll !== undefined) out.enableInfiniteScroll = enableInfiniteScroll;
|
|
186
|
+
|
|
187
|
+
const infiniteScrollUp = parseBoolean(el, 'infinite-scroll-up');
|
|
188
|
+
if (infiniteScrollUp !== undefined) out.infiniteScrollUp = infiniteScrollUp;
|
|
189
|
+
|
|
190
|
+
const infiniteScrollDown = parseBoolean(el, 'infinite-scroll-down');
|
|
191
|
+
if (infiniteScrollDown !== undefined) out.infiniteScrollDown = infiniteScrollDown;
|
|
192
|
+
|
|
193
|
+
// Selection — most flags default to true in the old grid, so we use
|
|
194
|
+
// the tri-state parser to let consumers opt out with attr="false".
|
|
195
|
+
const enableRowSelection = parseTriStateBoolean(el, 'enable-row-selection');
|
|
196
|
+
if (enableRowSelection !== undefined) out.enableRowSelection = enableRowSelection;
|
|
197
|
+
|
|
198
|
+
const multiSelect = parseTriStateBoolean(el, 'multi-select');
|
|
199
|
+
if (multiSelect !== undefined) out.multiSelect = multiSelect;
|
|
200
|
+
|
|
201
|
+
const noUnselect = parseTriStateBoolean(el, 'no-unselect');
|
|
202
|
+
if (noUnselect !== undefined) out.noUnselect = noUnselect;
|
|
203
|
+
|
|
204
|
+
const modifierKeysToMultiSelect = parseTriStateBoolean(el, 'modifier-keys-to-multi-select');
|
|
205
|
+
if (modifierKeysToMultiSelect !== undefined) out.modifierKeysToMultiSelect = modifierKeysToMultiSelect;
|
|
206
|
+
|
|
207
|
+
const enableRowHeaderSelection = parseTriStateBoolean(el, 'enable-row-header-selection');
|
|
208
|
+
if (enableRowHeaderSelection !== undefined) out.enableRowHeaderSelection = enableRowHeaderSelection;
|
|
209
|
+
|
|
210
|
+
const enableFullRowSelection = parseTriStateBoolean(el, 'enable-full-row-selection');
|
|
211
|
+
if (enableFullRowSelection !== undefined) out.enableFullRowSelection = enableFullRowSelection;
|
|
212
|
+
|
|
213
|
+
const enableFocusRowOnRowHeaderClick = parseTriStateBoolean(
|
|
214
|
+
el,
|
|
215
|
+
'enable-focus-row-on-row-header-click',
|
|
216
|
+
);
|
|
217
|
+
if (enableFocusRowOnRowHeaderClick !== undefined) out.enableFocusRowOnRowHeaderClick = enableFocusRowOnRowHeaderClick;
|
|
218
|
+
|
|
219
|
+
const enableSelectRowOnFocus = parseTriStateBoolean(el, 'enable-select-row-on-focus');
|
|
220
|
+
if (enableSelectRowOnFocus !== undefined) out.enableSelectRowOnFocus = enableSelectRowOnFocus;
|
|
221
|
+
|
|
222
|
+
const enableSelectAll = parseTriStateBoolean(el, 'enable-select-all');
|
|
223
|
+
if (enableSelectAll !== undefined) out.enableSelectAll = enableSelectAll;
|
|
224
|
+
|
|
225
|
+
const enableSelectionBatchEvent = parseTriStateBoolean(el, 'enable-selection-batch-event');
|
|
226
|
+
if (enableSelectionBatchEvent !== undefined) out.enableSelectionBatchEvent = enableSelectionBatchEvent;
|
|
227
|
+
|
|
228
|
+
const enableFooterTotalSelected = parseTriStateBoolean(el, 'enable-footer-total-selected');
|
|
229
|
+
if (enableFooterTotalSelected !== undefined) out.enableFooterTotalSelected = enableFooterTotalSelected;
|
|
230
|
+
|
|
231
|
+
const selectionRowHeaderWidth = parseNumber(el, 'selection-row-header-width');
|
|
232
|
+
if (selectionRowHeaderWidth !== undefined) out.selectionRowHeaderWidth = selectionRowHeaderWidth;
|
|
233
|
+
|
|
234
|
+
// JSON attributes
|
|
235
|
+
const columnDefs = parseJson<GridColumnDef[]>(el, 'column-defs');
|
|
236
|
+
if (columnDefs !== undefined) out.columnDefs = columnDefs;
|
|
237
|
+
|
|
238
|
+
const data = parseJson<GridRecord[]>(el, 'data');
|
|
239
|
+
if (data !== undefined) out.data = data;
|
|
240
|
+
|
|
241
|
+
const grouping = parseJson(el, 'grouping');
|
|
242
|
+
if (grouping !== undefined && grouping !== null) {
|
|
243
|
+
// Grouping is typed as `GridGroupingOptions` in core; we stay loose
|
|
244
|
+
// here because the parse-time check already validated shape via
|
|
245
|
+
// JSON.parse, and downstream merges don't care about the cast.
|
|
246
|
+
out.grouping = grouping as GridOptions['grouping'];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const paginationPageSizes = parseJson<number[] | null>(el, 'pagination-page-sizes');
|
|
250
|
+
if (paginationPageSizes !== undefined) out.paginationPageSizes = paginationPageSizes;
|
|
251
|
+
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Present attribute → true. Absent → undefined (no override). */
|
|
256
|
+
export function parseBoolean(el: Element, name: string): boolean | undefined {
|
|
257
|
+
return el.hasAttribute(name) ? true : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Tri-state boolean. Present + empty/"true" → true; "false"/"0"/"off" →
|
|
262
|
+
* false; absent → undefined. Use for flags that default to true in the
|
|
263
|
+
* model so consumers can opt out with `attr="false"` without removing
|
|
264
|
+
* the attribute.
|
|
265
|
+
*/
|
|
266
|
+
export function parseTriStateBoolean(el: Element, name: string): boolean | undefined {
|
|
267
|
+
if (!el.hasAttribute(name)) return undefined;
|
|
268
|
+
const raw = el.getAttribute(name);
|
|
269
|
+
if (raw === null) return undefined;
|
|
270
|
+
const normalized = raw.trim().toLowerCase();
|
|
271
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'off') return false;
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function parseNumber(el: Element, name: string): number | undefined {
|
|
276
|
+
const raw = el.getAttribute(name);
|
|
277
|
+
if (raw === null) return undefined;
|
|
278
|
+
const parsed = Number(raw);
|
|
279
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function parseJson<T = unknown>(el: Element, name: string): T | undefined {
|
|
283
|
+
const raw = el.getAttribute(name);
|
|
284
|
+
if (raw === null) return undefined;
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(raw) as T;
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.warn(`<ui-grid-element>: invalid JSON in "${name}" attribute`, e);
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
defineStandaloneUiGridElement,
|
|
4
|
+
mountVanillaUiGrid,
|
|
5
|
+
type GridOptions,
|
|
6
|
+
} from './index';
|
|
7
|
+
|
|
8
|
+
function createRows(count: number) {
|
|
9
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
10
|
+
id: `row-${i}`,
|
|
11
|
+
name: `Row ${i}`,
|
|
12
|
+
value: i * 10,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BASE_OPTIONS: GridOptions = {
|
|
17
|
+
id: 'auto-height-test',
|
|
18
|
+
columnDefs: [
|
|
19
|
+
{ name: 'name', displayName: 'Name' },
|
|
20
|
+
{ name: 'value', displayName: 'Value' },
|
|
21
|
+
],
|
|
22
|
+
data: createRows(50),
|
|
23
|
+
rowHeight: 40,
|
|
24
|
+
headerRowHeight: 50,
|
|
25
|
+
enableSorting: false,
|
|
26
|
+
enableFiltering: false,
|
|
27
|
+
enableVirtualization: true,
|
|
28
|
+
virtualizationThreshold: 1,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('autoAdjustHeight (matches old ui-grid enableMinHeightCheck)', () => {
|
|
32
|
+
let container: HTMLElement;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
container = document.createElement('div');
|
|
36
|
+
document.body.appendChild(container);
|
|
37
|
+
await defineStandaloneUiGridElement('ui-grid-auto-height-test');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
document.body.removeChild(container);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('sets host height when parent provides no height (minRowsToShow default=10)', async () => {
|
|
45
|
+
const grid = await mountVanillaUiGrid(
|
|
46
|
+
container,
|
|
47
|
+
{ ...BASE_OPTIONS },
|
|
48
|
+
undefined,
|
|
49
|
+
'ui-grid-auto-height-test',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Default minRowsToShow = 10, rowHeight = 40, headerRowHeight = 50
|
|
53
|
+
// Expected min height = 50 (header) + 0 (no filter) + 10 * 40 = 450
|
|
54
|
+
// Note: jsdom clientHeight is always 0, so autoAdjust always fires.
|
|
55
|
+
const expectedMin = 50 + 10 * 40;
|
|
56
|
+
expect(grid.style.height).toBe(`${expectedMin}px`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('respects custom minRowsToShow', async () => {
|
|
60
|
+
const grid = await mountVanillaUiGrid(
|
|
61
|
+
container,
|
|
62
|
+
{ ...BASE_OPTIONS, minRowsToShow: 5 },
|
|
63
|
+
undefined,
|
|
64
|
+
'ui-grid-auto-height-test',
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// 50 (header) + 5 * 40 = 250
|
|
68
|
+
const expectedMin = 50 + 5 * 40;
|
|
69
|
+
expect(grid.style.height).toBe(`${expectedMin}px`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('includes filter row height in calculation when filtering enabled', async () => {
|
|
73
|
+
const grid = await mountVanillaUiGrid(
|
|
74
|
+
container,
|
|
75
|
+
{ ...BASE_OPTIONS, enableFiltering: true, minRowsToShow: 5 },
|
|
76
|
+
undefined,
|
|
77
|
+
'ui-grid-auto-height-test',
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// With filtering, the filter strip adds height. The exact measured value
|
|
81
|
+
// depends on rendering, but the total should exceed header + rows alone.
|
|
82
|
+
const headerPlusRows = 50 + 5 * 40;
|
|
83
|
+
expect(parseInt(grid.style.height)).toBeGreaterThanOrEqual(headerPlusRows);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not adjust height when enableMinHeightCheck is false', async () => {
|
|
87
|
+
const grid = await mountVanillaUiGrid(
|
|
88
|
+
container,
|
|
89
|
+
{ ...BASE_OPTIONS, enableMinHeightCheck: false },
|
|
90
|
+
undefined,
|
|
91
|
+
'ui-grid-auto-height-test',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(grid.style.height).toBe('');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not override height when parent provides sufficient height', async () => {
|
|
98
|
+
container.style.height = '800px';
|
|
99
|
+
container.style.display = 'grid';
|
|
100
|
+
|
|
101
|
+
const grid = await mountVanillaUiGrid(
|
|
102
|
+
container,
|
|
103
|
+
{ ...BASE_OPTIONS },
|
|
104
|
+
undefined,
|
|
105
|
+
'ui-grid-auto-height-test',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Grid fills parent — autoAdjust should not fire because clientHeight >= minHeight
|
|
109
|
+
// Note: in jsdom clientHeight is always 0, so autoAdjust will fire.
|
|
110
|
+
// This test verifies the logic path exists; real browser testing
|
|
111
|
+
// confirms the parent-height case.
|
|
112
|
+
expect(grid.style.height).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('body viewport has overflow-y:auto for scrolling', async () => {
|
|
116
|
+
const grid = await mountVanillaUiGrid(
|
|
117
|
+
container,
|
|
118
|
+
{ ...BASE_OPTIONS },
|
|
119
|
+
undefined,
|
|
120
|
+
'ui-grid-auto-height-test',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const shadow = grid.shadowRoot!;
|
|
124
|
+
const bodyViewport = shadow.querySelector<HTMLElement>('.grid-body-viewport');
|
|
125
|
+
expect(bodyViewport).not.toBeNull();
|
|
126
|
+
expect(bodyViewport!.classList.contains('grid-body-viewport')).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('grid-frame fills the host element height', async () => {
|
|
130
|
+
const grid = await mountVanillaUiGrid(
|
|
131
|
+
container,
|
|
132
|
+
{ ...BASE_OPTIONS },
|
|
133
|
+
undefined,
|
|
134
|
+
'ui-grid-auto-height-test',
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const shadow = grid.shadowRoot!;
|
|
138
|
+
const gridFrame = shadow.querySelector<HTMLElement>('.grid-frame');
|
|
139
|
+
expect(gridFrame).not.toBeNull();
|
|
140
|
+
// .grid-frame has height:100% in CSS to fill the host
|
|
141
|
+
});
|
|
142
|
+
});
|