@alaarab/ogrid-vue 2.0.19 → 2.0.22
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/dist/esm/components/createDataGridTable.js +3 -0
- package/dist/esm/components/createInlineCellEditor.js +194 -0
- package/dist/esm/components/createOGrid.js +338 -0
- package/dist/esm/composables/useCellSelection.js +16 -4
- package/dist/esm/composables/useDataGridState.js +10 -8
- package/dist/esm/composables/useOGrid.js +19 -6
- package/dist/esm/composables/useRowSelection.js +2 -2
- package/dist/esm/index.js +4 -0
- package/dist/esm/styles/ogrid-layout.css +344 -0
- package/dist/esm/styles/ogrid-theme.css +52 -0
- package/dist/types/components/createInlineCellEditor.d.ts +75 -0
- package/dist/types/components/createOGrid.d.ts +35 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types/dataGridTypes.d.ts +4 -0
- package/package.json +8 -5
|
@@ -216,6 +216,9 @@ export function createDataGridTable(ui) {
|
|
|
216
216
|
backgroundColor: '#fff',
|
|
217
217
|
willChange: 'scroll-position',
|
|
218
218
|
};
|
|
219
|
+
if (p.rowHeight) {
|
|
220
|
+
wrapperStyle['--ogrid-row-height'] = `${p.rowHeight}px`;
|
|
221
|
+
}
|
|
219
222
|
return h('div', { class: 'ogrid-outer-container' }, [
|
|
220
223
|
// Scrollable wrapper
|
|
221
224
|
h('div', {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { defineComponent, ref, h, onMounted, nextTick, watch } from 'vue';
|
|
2
|
+
const editorWrapperStyle = {
|
|
3
|
+
width: '100%',
|
|
4
|
+
height: '100%',
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
padding: '0 2px',
|
|
8
|
+
boxSizing: 'border-box',
|
|
9
|
+
};
|
|
10
|
+
export function createInlineCellEditor(options) {
|
|
11
|
+
const { renderCheckbox, renderDatePicker } = options;
|
|
12
|
+
return defineComponent({
|
|
13
|
+
name: 'InlineCellEditor',
|
|
14
|
+
props: {
|
|
15
|
+
value: { default: undefined },
|
|
16
|
+
item: { type: Object, required: true },
|
|
17
|
+
column: { type: Object, required: true },
|
|
18
|
+
rowIndex: { type: Number, required: true },
|
|
19
|
+
editorType: { type: String, required: true },
|
|
20
|
+
onCommit: { type: Function, required: true },
|
|
21
|
+
onCancel: { type: Function, required: true },
|
|
22
|
+
},
|
|
23
|
+
setup(props) {
|
|
24
|
+
const inputRef = ref(null);
|
|
25
|
+
const selectWrapperRef = ref(null);
|
|
26
|
+
const selectDropdownRef = ref(null);
|
|
27
|
+
const localValue = ref(props.value);
|
|
28
|
+
const highlightedIndex = ref(0);
|
|
29
|
+
const positionDropdown = () => {
|
|
30
|
+
const wrapper = selectWrapperRef.value;
|
|
31
|
+
const dropdown = selectDropdownRef.value;
|
|
32
|
+
if (!wrapper || !dropdown)
|
|
33
|
+
return;
|
|
34
|
+
const rect = wrapper.getBoundingClientRect();
|
|
35
|
+
const maxH = 200;
|
|
36
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
37
|
+
const flipUp = spaceBelow < maxH && rect.top > spaceBelow;
|
|
38
|
+
dropdown.style.position = 'fixed';
|
|
39
|
+
dropdown.style.left = `${rect.left}px`;
|
|
40
|
+
dropdown.style.width = `${rect.width}px`;
|
|
41
|
+
dropdown.style.maxHeight = `${maxH}px`;
|
|
42
|
+
dropdown.style.zIndex = '9999';
|
|
43
|
+
dropdown.style.right = 'auto';
|
|
44
|
+
if (flipUp) {
|
|
45
|
+
dropdown.style.top = 'auto';
|
|
46
|
+
dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
dropdown.style.top = `${rect.bottom}px`;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
onMounted(() => {
|
|
53
|
+
nextTick(() => {
|
|
54
|
+
if (selectWrapperRef.value) {
|
|
55
|
+
selectWrapperRef.value.focus();
|
|
56
|
+
positionDropdown();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
inputRef.value?.focus();
|
|
60
|
+
inputRef.value?.select();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
// Sync local value when prop changes
|
|
64
|
+
watch(() => props.value, (v) => { localValue.value = v; });
|
|
65
|
+
// Initialize highlighted index to current value
|
|
66
|
+
const initHighlightedIndex = () => {
|
|
67
|
+
const values = props.column.cellEditorParams?.values ?? [];
|
|
68
|
+
const idx = values.findIndex((v) => String(v) === String(props.value));
|
|
69
|
+
highlightedIndex.value = Math.max(idx, 0);
|
|
70
|
+
};
|
|
71
|
+
initHighlightedIndex();
|
|
72
|
+
const scrollHighlightedIntoView = () => {
|
|
73
|
+
nextTick(() => {
|
|
74
|
+
const dropdown = selectDropdownRef.value;
|
|
75
|
+
if (!dropdown)
|
|
76
|
+
return;
|
|
77
|
+
const highlighted = dropdown.children[highlightedIndex.value];
|
|
78
|
+
highlighted?.scrollIntoView({ block: 'nearest' });
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const getDisplayText = (value) => {
|
|
82
|
+
const formatValue = props.column.cellEditorParams?.formatValue;
|
|
83
|
+
if (formatValue)
|
|
84
|
+
return formatValue(value);
|
|
85
|
+
return value != null ? String(value) : '';
|
|
86
|
+
};
|
|
87
|
+
const handleSelectKeyDown = (e) => {
|
|
88
|
+
const values = props.column.cellEditorParams?.values ?? [];
|
|
89
|
+
switch (e.key) {
|
|
90
|
+
case 'ArrowDown':
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
highlightedIndex.value = Math.min(highlightedIndex.value + 1, values.length - 1);
|
|
93
|
+
scrollHighlightedIntoView();
|
|
94
|
+
break;
|
|
95
|
+
case 'ArrowUp':
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0);
|
|
98
|
+
scrollHighlightedIntoView();
|
|
99
|
+
break;
|
|
100
|
+
case 'Enter':
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
if (values.length > 0 && highlightedIndex.value < values.length) {
|
|
104
|
+
props.onCommit(values[highlightedIndex.value]);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
case 'Tab':
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
if (values.length > 0 && highlightedIndex.value < values.length) {
|
|
110
|
+
props.onCommit(values[highlightedIndex.value]);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case 'Escape':
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
props.onCancel();
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
return () => {
|
|
121
|
+
if (props.editorType === 'checkbox') {
|
|
122
|
+
const checked = !!props.value;
|
|
123
|
+
return h('div', { style: { ...editorWrapperStyle, justifyContent: 'center' } }, renderCheckbox({
|
|
124
|
+
checked,
|
|
125
|
+
onChange: (c) => props.onCommit(c),
|
|
126
|
+
onCancel: props.onCancel,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
if (props.editorType === 'select') {
|
|
130
|
+
const values = props.column.cellEditorParams?.values ?? [];
|
|
131
|
+
return h('div', {
|
|
132
|
+
ref: (el) => { selectWrapperRef.value = el; },
|
|
133
|
+
tabindex: 0,
|
|
134
|
+
style: { ...editorWrapperStyle, position: 'relative' },
|
|
135
|
+
onKeydown: handleSelectKeyDown,
|
|
136
|
+
}, [
|
|
137
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', cursor: 'pointer', fontSize: '13px', color: 'inherit' } }, [
|
|
138
|
+
h('span', getDisplayText(props.value)),
|
|
139
|
+
h('span', { style: { marginLeft: '4px', fontSize: '10px', opacity: '0.5' } }, '\u25BE'),
|
|
140
|
+
]),
|
|
141
|
+
h('div', {
|
|
142
|
+
ref: (el) => { selectDropdownRef.value = el; },
|
|
143
|
+
role: 'listbox',
|
|
144
|
+
style: { position: 'absolute', top: '100%', left: '0', right: '0', maxHeight: '200px', overflowY: 'auto', background: 'var(--ogrid-bg, #fff)', border: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))', zIndex: '10', boxShadow: '0 4px 16px rgba(0,0,0,0.2)' },
|
|
145
|
+
}, values.map((v, i) => h('div', {
|
|
146
|
+
key: String(v),
|
|
147
|
+
role: 'option',
|
|
148
|
+
'aria-selected': i === highlightedIndex.value,
|
|
149
|
+
onClick: () => props.onCommit(v),
|
|
150
|
+
style: { padding: '6px 8px', cursor: 'pointer', color: 'var(--ogrid-fg, #242424)', ...(i === highlightedIndex.value ? { background: 'var(--ogrid-bg-hover, #e8f0fe)' } : {}) },
|
|
151
|
+
}, getDisplayText(v)))),
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
if (props.editorType === 'date') {
|
|
155
|
+
let dateStr = '';
|
|
156
|
+
if (localValue.value) {
|
|
157
|
+
const d = new Date(String(localValue.value));
|
|
158
|
+
if (!Number.isNaN(d.getTime())) {
|
|
159
|
+
dateStr = d.toISOString().slice(0, 10);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return h('div', { style: editorWrapperStyle }, renderDatePicker({
|
|
163
|
+
value: dateStr,
|
|
164
|
+
onChange: (val) => props.onCommit(val),
|
|
165
|
+
onCancel: props.onCancel,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
// Default: text editor
|
|
169
|
+
return h('div', { style: editorWrapperStyle }, h('input', {
|
|
170
|
+
ref: (el) => { inputRef.value = el; },
|
|
171
|
+
type: 'text',
|
|
172
|
+
value: localValue.value != null ? String(localValue.value) : '',
|
|
173
|
+
style: { width: '100%', height: '100%', border: 'none', outline: 'none', padding: '0 4px', fontSize: 'inherit', boxSizing: 'border-box' },
|
|
174
|
+
onInput: (e) => { localValue.value = e.target.value; },
|
|
175
|
+
onKeydown: (e) => {
|
|
176
|
+
if (e.key === 'Enter') {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
props.onCommit(localValue.value);
|
|
179
|
+
}
|
|
180
|
+
if (e.key === 'Escape') {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
props.onCancel();
|
|
183
|
+
}
|
|
184
|
+
if (e.key === 'Tab') {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
props.onCommit(localValue.value);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
onBlur: () => props.onCommit(localValue.value),
|
|
190
|
+
}));
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OGrid factory for Vue UI packages.
|
|
3
|
+
*
|
|
4
|
+
* Both vue-vuetify and vue-primevue OGrid components are 100% identical —
|
|
5
|
+
* they only differ in which DataGridTable, ColumnChooser, and PaginationControls
|
|
6
|
+
* components they use. This factory extracts all shared logic into one place.
|
|
7
|
+
*/
|
|
8
|
+
import { defineComponent, h, computed } from 'vue';
|
|
9
|
+
import { useOGrid, } from '../composables';
|
|
10
|
+
// --- SideBar constants and styles ---
|
|
11
|
+
const PANEL_WIDTH = 240;
|
|
12
|
+
const TAB_WIDTH = 36;
|
|
13
|
+
const PANEL_LABELS = {
|
|
14
|
+
columns: 'Columns',
|
|
15
|
+
filters: 'Filters',
|
|
16
|
+
};
|
|
17
|
+
const PANEL_ICONS = {
|
|
18
|
+
columns: '\u2261', // hamburger icon
|
|
19
|
+
filters: '\u2A65', // filter icon
|
|
20
|
+
};
|
|
21
|
+
/** Render the SideBar inline (tab strip + panel content). */
|
|
22
|
+
function renderSideBar(sb) {
|
|
23
|
+
const isOpen = sb.activePanel !== null;
|
|
24
|
+
const position = sb.position ?? 'right';
|
|
25
|
+
const tabStripStyle = {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
flexDirection: 'column',
|
|
28
|
+
width: `${TAB_WIDTH}px`,
|
|
29
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
30
|
+
...(position === 'right'
|
|
31
|
+
? { borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' }
|
|
32
|
+
: { borderRight: '1px solid var(--ogrid-border, #e0e0e0)' }),
|
|
33
|
+
};
|
|
34
|
+
const tabStrip = h('div', { style: tabStripStyle, role: 'tablist', 'aria-label': 'Side bar tabs' }, sb.panels.map((panel) => h('button', {
|
|
35
|
+
key: panel,
|
|
36
|
+
role: 'tab',
|
|
37
|
+
'aria-selected': sb.activePanel === panel,
|
|
38
|
+
'aria-label': PANEL_LABELS[panel],
|
|
39
|
+
title: PANEL_LABELS[panel],
|
|
40
|
+
onClick: () => sb.onPanelChange(sb.activePanel === panel ? null : panel),
|
|
41
|
+
style: {
|
|
42
|
+
width: `${TAB_WIDTH}px`,
|
|
43
|
+
height: `${TAB_WIDTH}px`,
|
|
44
|
+
border: 'none',
|
|
45
|
+
cursor: 'pointer',
|
|
46
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
47
|
+
fontSize: '14px',
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
background: sb.activePanel === panel ? 'var(--ogrid-bg, #fff)' : 'transparent',
|
|
52
|
+
fontWeight: sb.activePanel === panel ? 'bold' : 'normal',
|
|
53
|
+
},
|
|
54
|
+
}, PANEL_ICONS[panel])));
|
|
55
|
+
let panelContent = null;
|
|
56
|
+
if (isOpen && sb.activePanel) {
|
|
57
|
+
const panelContainerStyle = {
|
|
58
|
+
width: `${PANEL_WIDTH}px`,
|
|
59
|
+
display: 'flex',
|
|
60
|
+
flexDirection: 'column',
|
|
61
|
+
overflow: 'hidden',
|
|
62
|
+
background: 'var(--ogrid-bg, #fff)',
|
|
63
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
64
|
+
...(position === 'right'
|
|
65
|
+
? { borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' }
|
|
66
|
+
: { borderRight: '1px solid var(--ogrid-border, #e0e0e0)' }),
|
|
67
|
+
};
|
|
68
|
+
const panelBodyChildren = [];
|
|
69
|
+
if (sb.activePanel === 'columns') {
|
|
70
|
+
const allVisible = sb.columns.every((c) => sb.visibleColumns.has(c.columnId));
|
|
71
|
+
// Select All / Clear All buttons
|
|
72
|
+
panelBodyChildren.push(h('div', { style: { display: 'flex', gap: '8px', marginBottom: '8px' } }, [
|
|
73
|
+
h('button', {
|
|
74
|
+
disabled: allVisible,
|
|
75
|
+
onClick: () => {
|
|
76
|
+
const next = new Set(sb.visibleColumns);
|
|
77
|
+
sb.columns.forEach((c) => next.add(c.columnId));
|
|
78
|
+
sb.onSetVisibleColumns(next);
|
|
79
|
+
},
|
|
80
|
+
style: { flex: '1', cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px', padding: '4px 8px' },
|
|
81
|
+
}, 'Select All'),
|
|
82
|
+
h('button', {
|
|
83
|
+
onClick: () => {
|
|
84
|
+
const next = new Set();
|
|
85
|
+
sb.columns.forEach((c) => {
|
|
86
|
+
if (c.required && sb.visibleColumns.has(c.columnId))
|
|
87
|
+
next.add(c.columnId);
|
|
88
|
+
});
|
|
89
|
+
sb.onSetVisibleColumns(next);
|
|
90
|
+
},
|
|
91
|
+
style: { flex: '1', cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px', padding: '4px 8px' },
|
|
92
|
+
}, 'Clear All'),
|
|
93
|
+
]));
|
|
94
|
+
// Column checkboxes
|
|
95
|
+
sb.columns.forEach((col) => {
|
|
96
|
+
panelBodyChildren.push(h('label', { key: col.columnId, style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '2px 0', cursor: 'pointer' } }, [
|
|
97
|
+
h('input', {
|
|
98
|
+
type: 'checkbox',
|
|
99
|
+
checked: sb.visibleColumns.has(col.columnId),
|
|
100
|
+
disabled: col.required,
|
|
101
|
+
onChange: (e) => sb.onVisibilityChange(col.columnId, e.target.checked),
|
|
102
|
+
}),
|
|
103
|
+
h('span', null, col.name),
|
|
104
|
+
]));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (sb.activePanel === 'filters') {
|
|
108
|
+
if (sb.filterableColumns.length === 0) {
|
|
109
|
+
panelBodyChildren.push(h('div', { style: { color: 'var(--ogrid-muted, #999)', fontStyle: 'italic' } }, 'No filterable columns'));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sb.filterableColumns.forEach((col) => {
|
|
113
|
+
const filterKey = col.filterField;
|
|
114
|
+
const groupChildren = [
|
|
115
|
+
h('div', { style: { fontWeight: '500', marginBottom: '4px', fontSize: '13px' } }, col.name),
|
|
116
|
+
];
|
|
117
|
+
if (col.filterType === 'text') {
|
|
118
|
+
const currentVal = sb.filters[filterKey]?.type === 'text' ? sb.filters[filterKey].value : '';
|
|
119
|
+
groupChildren.push(h('input', {
|
|
120
|
+
type: 'text',
|
|
121
|
+
value: currentVal,
|
|
122
|
+
onInput: (e) => {
|
|
123
|
+
const val = e.target.value;
|
|
124
|
+
sb.onFilterChange(filterKey, val ? { type: 'text', value: val } : undefined);
|
|
125
|
+
},
|
|
126
|
+
placeholder: `Filter ${col.name}...`,
|
|
127
|
+
'aria-label': `Filter ${col.name}`,
|
|
128
|
+
style: { width: '100%', boxSizing: 'border-box', padding: '4px 6px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px' },
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
if (col.filterType === 'multiSelect') {
|
|
132
|
+
const options = sb.filterOptions[filterKey] ?? [];
|
|
133
|
+
const msChildren = options.map((opt) => {
|
|
134
|
+
const selected = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value.includes(opt) : false;
|
|
135
|
+
return h('label', { key: opt, style: { display: 'flex', alignItems: 'center', gap: '4px', padding: '1px 0', cursor: 'pointer', fontSize: '13px' } }, [
|
|
136
|
+
h('input', {
|
|
137
|
+
type: 'checkbox',
|
|
138
|
+
checked: selected,
|
|
139
|
+
onChange: (e) => {
|
|
140
|
+
const current = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value : [];
|
|
141
|
+
const next = e.target.checked
|
|
142
|
+
? [...current, opt]
|
|
143
|
+
: current.filter((v) => v !== opt);
|
|
144
|
+
sb.onFilterChange(filterKey, next.length > 0 ? { type: 'multiSelect', value: next } : undefined);
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
h('span', null, opt),
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
groupChildren.push(h('div', { style: { maxHeight: '120px', overflowY: 'auto' }, role: 'group', 'aria-label': `${col.name} options` }, msChildren));
|
|
151
|
+
}
|
|
152
|
+
if (col.filterType === 'date') {
|
|
153
|
+
const existingValue = sb.filters[filterKey]?.type === 'date' ? sb.filters[filterKey].value : { from: undefined, to: undefined };
|
|
154
|
+
groupChildren.push(h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, [
|
|
155
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } }, [
|
|
156
|
+
'From:',
|
|
157
|
+
h('input', {
|
|
158
|
+
type: 'date',
|
|
159
|
+
value: existingValue.from ?? '',
|
|
160
|
+
onInput: (e) => {
|
|
161
|
+
const from = e.target.value || undefined;
|
|
162
|
+
const to = existingValue.to;
|
|
163
|
+
sb.onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
|
|
164
|
+
},
|
|
165
|
+
'aria-label': `${col.name} from date`,
|
|
166
|
+
style: { flex: '1', padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px' },
|
|
167
|
+
}),
|
|
168
|
+
]),
|
|
169
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } }, [
|
|
170
|
+
'To:',
|
|
171
|
+
h('input', {
|
|
172
|
+
type: 'date',
|
|
173
|
+
value: existingValue.to ?? '',
|
|
174
|
+
onInput: (e) => {
|
|
175
|
+
const to = e.target.value || undefined;
|
|
176
|
+
const from = existingValue.from;
|
|
177
|
+
sb.onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
|
|
178
|
+
},
|
|
179
|
+
'aria-label': `${col.name} to date`,
|
|
180
|
+
style: { flex: '1', padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px' },
|
|
181
|
+
}),
|
|
182
|
+
]),
|
|
183
|
+
]));
|
|
184
|
+
}
|
|
185
|
+
panelBodyChildren.push(h('div', { key: col.columnId, style: { marginBottom: '12px' } }, groupChildren));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
panelContent = h('div', { role: 'tabpanel', 'aria-label': PANEL_LABELS[sb.activePanel], style: panelContainerStyle }, [
|
|
190
|
+
// Panel header
|
|
191
|
+
h('div', {
|
|
192
|
+
style: {
|
|
193
|
+
display: 'flex',
|
|
194
|
+
justifyContent: 'space-between',
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
padding: '8px 12px',
|
|
197
|
+
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
198
|
+
fontWeight: '600',
|
|
199
|
+
},
|
|
200
|
+
}, [
|
|
201
|
+
h('span', null, PANEL_LABELS[sb.activePanel]),
|
|
202
|
+
h('button', {
|
|
203
|
+
onClick: () => sb.onPanelChange(null),
|
|
204
|
+
style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '16px', color: 'var(--ogrid-fg, #242424)' },
|
|
205
|
+
'aria-label': 'Close panel',
|
|
206
|
+
}, '\u00D7'),
|
|
207
|
+
]),
|
|
208
|
+
// Panel body
|
|
209
|
+
h('div', { style: { flex: '1', overflowY: 'auto', padding: '8px 12px' } }, panelBodyChildren),
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
212
|
+
const children = [];
|
|
213
|
+
if (position === 'left') {
|
|
214
|
+
children.push(tabStrip);
|
|
215
|
+
if (panelContent)
|
|
216
|
+
children.push(panelContent);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
if (panelContent)
|
|
220
|
+
children.push(panelContent);
|
|
221
|
+
children.push(tabStrip);
|
|
222
|
+
}
|
|
223
|
+
return h('div', {
|
|
224
|
+
style: { display: 'flex', flexDirection: 'row', flexShrink: '0' },
|
|
225
|
+
role: 'complementary',
|
|
226
|
+
'aria-label': 'Side bar',
|
|
227
|
+
}, children);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Creates an OGrid component with framework-specific UI bindings.
|
|
231
|
+
* All orchestration logic, sidebar, toolbar, and layout are shared.
|
|
232
|
+
*/
|
|
233
|
+
export function createOGrid(ui) {
|
|
234
|
+
return defineComponent({
|
|
235
|
+
name: 'OGrid',
|
|
236
|
+
props: {
|
|
237
|
+
gridProps: { type: Object, required: true },
|
|
238
|
+
},
|
|
239
|
+
setup(props, { expose }) {
|
|
240
|
+
const propsRef = computed(() => props.gridProps);
|
|
241
|
+
const { dataGridProps, pagination, columnChooser, layout, api } = useOGrid(propsRef);
|
|
242
|
+
// Expose the API for parent refs
|
|
243
|
+
expose({ api: api.value });
|
|
244
|
+
return () => {
|
|
245
|
+
const sideBar = layout.value.sideBarProps;
|
|
246
|
+
const hasSideBar = sideBar != null;
|
|
247
|
+
const sideBarPosition = sideBar?.position ?? 'right';
|
|
248
|
+
// Toolbar
|
|
249
|
+
const toolbarChildren = [];
|
|
250
|
+
if (layout.value.toolbar) {
|
|
251
|
+
toolbarChildren.push(layout.value.toolbar);
|
|
252
|
+
}
|
|
253
|
+
// ColumnChooser in toolbar
|
|
254
|
+
const toolbarEnd = columnChooser.value.placement === 'toolbar'
|
|
255
|
+
? h(ui.ColumnChooser, {
|
|
256
|
+
columns: columnChooser.value.columns,
|
|
257
|
+
visibleColumns: columnChooser.value.visibleColumns,
|
|
258
|
+
onVisibilityChange: columnChooser.value.onVisibilityChange,
|
|
259
|
+
})
|
|
260
|
+
: null;
|
|
261
|
+
// Pagination
|
|
262
|
+
const paginationNode = h(ui.PaginationControls, {
|
|
263
|
+
currentPage: pagination.value.page,
|
|
264
|
+
pageSize: pagination.value.pageSize,
|
|
265
|
+
totalCount: pagination.value.displayTotalCount,
|
|
266
|
+
onPageChange: pagination.value.setPage,
|
|
267
|
+
onPageSizeChange: (size) => {
|
|
268
|
+
pagination.value.setPageSize(size);
|
|
269
|
+
pagination.value.setPage(1);
|
|
270
|
+
},
|
|
271
|
+
pageSizeOptions: pagination.value.pageSizeOptions,
|
|
272
|
+
entityLabelPlural: pagination.value.entityLabelPlural,
|
|
273
|
+
});
|
|
274
|
+
// Grid content area
|
|
275
|
+
const gridChild = h('div', {
|
|
276
|
+
style: { flex: '1', minWidth: '0', minHeight: '0', display: 'flex', flexDirection: 'column' },
|
|
277
|
+
}, [
|
|
278
|
+
h(ui.DataGridTable, {
|
|
279
|
+
gridProps: dataGridProps.value,
|
|
280
|
+
}),
|
|
281
|
+
]);
|
|
282
|
+
// Main content area (sidebar + grid)
|
|
283
|
+
const mainAreaChildren = [];
|
|
284
|
+
if (hasSideBar && sideBarPosition === 'left') {
|
|
285
|
+
mainAreaChildren.push(renderSideBar(sideBar));
|
|
286
|
+
}
|
|
287
|
+
mainAreaChildren.push(gridChild);
|
|
288
|
+
if (hasSideBar && sideBarPosition !== 'left') {
|
|
289
|
+
mainAreaChildren.push(renderSideBar(sideBar));
|
|
290
|
+
}
|
|
291
|
+
return h('div', {
|
|
292
|
+
class: layout.value.className,
|
|
293
|
+
style: {
|
|
294
|
+
display: 'flex',
|
|
295
|
+
flexDirection: 'column',
|
|
296
|
+
border: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
|
|
297
|
+
borderRadius: '4px',
|
|
298
|
+
overflow: 'hidden',
|
|
299
|
+
},
|
|
300
|
+
}, [
|
|
301
|
+
// Toolbar strip
|
|
302
|
+
...(toolbarChildren.length || toolbarEnd ? [
|
|
303
|
+
h('div', {
|
|
304
|
+
style: {
|
|
305
|
+
display: 'flex',
|
|
306
|
+
alignItems: 'center',
|
|
307
|
+
justifyContent: 'space-between',
|
|
308
|
+
padding: '8px 12px',
|
|
309
|
+
borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
|
|
310
|
+
gap: '8px',
|
|
311
|
+
},
|
|
312
|
+
}, [
|
|
313
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '1' } }, toolbarChildren),
|
|
314
|
+
...(toolbarEnd ? [toolbarEnd] : []),
|
|
315
|
+
]),
|
|
316
|
+
] : []),
|
|
317
|
+
// Below toolbar strip
|
|
318
|
+
...(layout.value.toolbarBelow ? [
|
|
319
|
+
h('div', {
|
|
320
|
+
style: { padding: '8px 12px', borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))' },
|
|
321
|
+
}, [layout.value.toolbarBelow]),
|
|
322
|
+
] : []),
|
|
323
|
+
// Main content area (sidebar + grid)
|
|
324
|
+
h('div', { style: { display: 'flex', flex: '1', minHeight: '0' } }, mainAreaChildren),
|
|
325
|
+
// Footer strip (pagination)
|
|
326
|
+
h('div', {
|
|
327
|
+
style: {
|
|
328
|
+
display: 'flex',
|
|
329
|
+
alignItems: 'center',
|
|
330
|
+
padding: '8px 0',
|
|
331
|
+
borderTop: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
|
|
332
|
+
},
|
|
333
|
+
}, [paginationNode]),
|
|
334
|
+
]);
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
@@ -286,13 +286,25 @@ export function useCellSelection(params) {
|
|
|
286
286
|
if (wasDrag)
|
|
287
287
|
isDragging.value = false;
|
|
288
288
|
};
|
|
289
|
+
let isUnmounted = false;
|
|
290
|
+
const onMoveSafe = (e) => {
|
|
291
|
+
if (isUnmounted)
|
|
292
|
+
return;
|
|
293
|
+
onMove(e);
|
|
294
|
+
};
|
|
295
|
+
const onUpSafe = () => {
|
|
296
|
+
if (isUnmounted)
|
|
297
|
+
return;
|
|
298
|
+
onUp();
|
|
299
|
+
};
|
|
289
300
|
onMounted(() => {
|
|
290
|
-
window.addEventListener('mousemove',
|
|
291
|
-
window.addEventListener('mouseup',
|
|
301
|
+
window.addEventListener('mousemove', onMoveSafe, true);
|
|
302
|
+
window.addEventListener('mouseup', onUpSafe, true);
|
|
292
303
|
});
|
|
293
304
|
onUnmounted(() => {
|
|
294
|
-
|
|
295
|
-
window.removeEventListener('
|
|
305
|
+
isUnmounted = true;
|
|
306
|
+
window.removeEventListener('mousemove', onMoveSafe, true);
|
|
307
|
+
window.removeEventListener('mouseup', onUpSafe, true);
|
|
296
308
|
if (rafId)
|
|
297
309
|
cancelAnimationFrame(rafId);
|
|
298
310
|
stopAutoScroll();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, computed, watch, nextTick } from 'vue';
|
|
1
|
+
import { ref, shallowRef, computed, watch, nextTick, triggerRef } from 'vue';
|
|
2
2
|
import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
3
3
|
import { useRowSelection } from './useRowSelection';
|
|
4
4
|
import { useCellEditing } from './useCellEditing';
|
|
@@ -26,7 +26,7 @@ export function useDataGridState(params) {
|
|
|
26
26
|
const { props, wrapperRef } = params;
|
|
27
27
|
const items = computed(() => props.value.items);
|
|
28
28
|
const columnsProp = computed(() => props.value.columns);
|
|
29
|
-
const getRowId =
|
|
29
|
+
const getRowId = props.value.getRowId; // stable function reference, no reactivity needed
|
|
30
30
|
const visibleColumnsProp = computed(() => props.value.visibleColumns);
|
|
31
31
|
const columnOrderProp = computed(() => props.value.columnOrder);
|
|
32
32
|
const rowSelectionProp = computed(() => props.value.rowSelection ?? 'none');
|
|
@@ -86,12 +86,14 @@ export function useDataGridState(params) {
|
|
|
86
86
|
const hasRowNumbersCol = computed(() => !!props.value.showRowNumbers);
|
|
87
87
|
const specialColsCount = computed(() => (hasCheckboxCol.value ? 1 : 0) + (hasRowNumbersCol.value ? 1 : 0));
|
|
88
88
|
const totalColCount = computed(() => visibleColumnCount.value + specialColsCount.value);
|
|
89
|
-
const colOffset =
|
|
90
|
-
const rowIndexByRowId =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
const colOffset = specialColsCount.value; // snapshot: checkbox/rowNumbers cols are fixed at setup
|
|
90
|
+
const rowIndexByRowId = shallowRef(new Map());
|
|
91
|
+
watch(items, (newItems) => {
|
|
92
|
+
const m = rowIndexByRowId.value;
|
|
93
|
+
m.clear();
|
|
94
|
+
newItems.forEach((item, idx) => m.set(getRowId(item), idx));
|
|
95
|
+
triggerRef(rowIndexByRowId);
|
|
96
|
+
}, { immediate: true });
|
|
95
97
|
const rowSelectionResult = useRowSelection({
|
|
96
98
|
items,
|
|
97
99
|
getRowId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, computed, watch } from 'vue';
|
|
1
|
+
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
|
2
2
|
import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, computeNextSortState, } from '@alaarab/ogrid-core';
|
|
3
3
|
import { useFilterOptions } from './useFilterOptions';
|
|
4
4
|
import { useSideBarState } from './useSideBarState';
|
|
@@ -173,8 +173,9 @@ export function useOGrid(props) {
|
|
|
173
173
|
const serverTotalCount = ref(0);
|
|
174
174
|
const loading = ref(true);
|
|
175
175
|
let fetchId = 0;
|
|
176
|
+
let isDestroyed = false;
|
|
176
177
|
const refreshCounter = ref(0);
|
|
177
|
-
|
|
178
|
+
const doFetch = () => {
|
|
178
179
|
if (!isServerSide.value || !dataProps.value.dataSource) {
|
|
179
180
|
if (!isServerSide.value)
|
|
180
181
|
loading.value = false;
|
|
@@ -190,23 +191,34 @@ export function useOGrid(props) {
|
|
|
190
191
|
filters: filters.value,
|
|
191
192
|
})
|
|
192
193
|
.then((res) => {
|
|
193
|
-
if (id !== fetchId)
|
|
194
|
+
if (id !== fetchId || isDestroyed)
|
|
194
195
|
return;
|
|
195
196
|
serverItems.value = res.items;
|
|
196
197
|
serverTotalCount.value = res.totalCount;
|
|
197
198
|
})
|
|
198
199
|
.catch((err) => {
|
|
199
|
-
if (id !== fetchId)
|
|
200
|
+
if (id !== fetchId || isDestroyed)
|
|
200
201
|
return;
|
|
201
202
|
callbacks.value.onError?.(err);
|
|
202
203
|
serverItems.value = [];
|
|
203
204
|
serverTotalCount.value = 0;
|
|
204
205
|
})
|
|
205
206
|
.finally(() => {
|
|
206
|
-
if (id === fetchId)
|
|
207
|
+
if (id === fetchId && !isDestroyed)
|
|
207
208
|
loading.value = false;
|
|
208
209
|
});
|
|
209
|
-
}
|
|
210
|
+
};
|
|
211
|
+
// Initial fetch on mount
|
|
212
|
+
onMounted(() => {
|
|
213
|
+
doFetch();
|
|
214
|
+
});
|
|
215
|
+
// Subsequent fetches on page/sort/filter changes (no immediate — onMounted handles initial)
|
|
216
|
+
watch([() => dataProps.value.dataSource, page, pageSize, () => sort.value.field, () => sort.value.direction, filters, refreshCounter], () => {
|
|
217
|
+
doFetch();
|
|
218
|
+
});
|
|
219
|
+
onUnmounted(() => {
|
|
220
|
+
isDestroyed = true;
|
|
221
|
+
});
|
|
210
222
|
const displayItems = computed(() => isClientSide.value && clientItemsAndTotal.value
|
|
211
223
|
? clientItemsAndTotal.value.items
|
|
212
224
|
: serverItems.value);
|
|
@@ -339,6 +351,7 @@ export function useOGrid(props) {
|
|
|
339
351
|
suppressHorizontalScroll: p.suppressHorizontalScroll,
|
|
340
352
|
columnReorder: p.columnReorder,
|
|
341
353
|
virtualScroll: p.virtualScroll,
|
|
354
|
+
rowHeight: p.rowHeight,
|
|
342
355
|
density: p.density ?? 'normal',
|
|
343
356
|
'aria-label': p['aria-label'],
|
|
344
357
|
'aria-labelledby': p['aria-labelledby'],
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { shallowRef, computed } from 'vue';
|
|
2
2
|
/**
|
|
3
3
|
* Manages row selection state for single or multiple selection modes with shift-click range support.
|
|
4
4
|
*/
|
|
5
5
|
export function useRowSelection(params) {
|
|
6
6
|
const { items, getRowId, rowSelection, controlledSelectedRows, onSelectionChange, } = params;
|
|
7
|
-
const internalSelectedRows =
|
|
7
|
+
const internalSelectedRows = shallowRef(new Set());
|
|
8
8
|
let lastClickedRow = -1;
|
|
9
9
|
const selectedRowIds = computed(() => {
|
|
10
10
|
const controlled = controlledSelectedRows.value;
|
package/dist/esm/index.js
CHANGED
|
@@ -10,3 +10,7 @@ export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelec
|
|
|
10
10
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
|
|
11
11
|
// DataGridTable factory (for UI packages)
|
|
12
12
|
export { createDataGridTable } from './components/createDataGridTable';
|
|
13
|
+
// InlineCellEditor factory (for UI packages)
|
|
14
|
+
export { createInlineCellEditor } from './components/createInlineCellEditor';
|
|
15
|
+
// OGrid factory (for UI packages)
|
|
16
|
+
export { createOGrid } from './components/createOGrid';
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/* OGrid Shared Layout Styles — consumed by vue-vuetify and vue-primevue */
|
|
2
|
+
|
|
3
|
+
/* Remove focus outline from scrollable wrapper (keyboard nav is handled via cell outlines) */
|
|
4
|
+
[role="region"][tabindex="0"] {
|
|
5
|
+
outline: none !important;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Cell selection highlighting */
|
|
9
|
+
.ogrid-cell-in-range {
|
|
10
|
+
background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Cut range highlighting */
|
|
14
|
+
.ogrid-cell-cut {
|
|
15
|
+
background: var(--ogrid-hover-bg) !important;
|
|
16
|
+
opacity: 0.7;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Drag-range highlight applied via DOM attributes during drag (bypasses Vue for performance) */
|
|
20
|
+
[data-drag-range] {
|
|
21
|
+
background: var(--ogrid-range-bg, rgba(33, 115, 70, 0.12)) !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Anchor cell during drag: white/transparent background (like Excel) */
|
|
25
|
+
[data-drag-anchor] {
|
|
26
|
+
background: var(--ogrid-bg) !important;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* === Layout === */
|
|
30
|
+
|
|
31
|
+
.ogrid-outer-container {
|
|
32
|
+
position: relative;
|
|
33
|
+
flex: 1;
|
|
34
|
+
min-height: 0;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
background-color: var(--ogrid-bg);
|
|
38
|
+
color: var(--ogrid-fg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ogrid-scroll-wrapper {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
min-height: 100%;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ogrid-table-container {
|
|
48
|
+
position: relative;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ogrid-table-container--loading {
|
|
52
|
+
opacity: 0.6;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ogrid-table {
|
|
56
|
+
width: 100%;
|
|
57
|
+
border-collapse: collapse;
|
|
58
|
+
font-size: 0.875rem;
|
|
59
|
+
background-color: var(--ogrid-bg);
|
|
60
|
+
color: var(--ogrid-fg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ogrid-table tbody tr {
|
|
64
|
+
height: var(--ogrid-row-height, auto);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* === Header === */
|
|
68
|
+
|
|
69
|
+
.ogrid-thead {
|
|
70
|
+
z-index: 8;
|
|
71
|
+
background-color: var(--ogrid-header-bg);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ogrid-header-row {
|
|
75
|
+
background-color: var(--ogrid-header-bg);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ogrid-header-cell {
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
position: sticky;
|
|
81
|
+
top: 0;
|
|
82
|
+
background-color: var(--ogrid-header-bg);
|
|
83
|
+
z-index: 8;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ogrid-header-cell--pinned-left {
|
|
87
|
+
z-index: 10;
|
|
88
|
+
will-change: transform;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ogrid-header-cell--pinned-right {
|
|
92
|
+
z-index: 10;
|
|
93
|
+
will-change: transform;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.ogrid-header-content {
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
width: 100%;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.ogrid-column-group-header {
|
|
103
|
+
text-align: center;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
border-bottom: 2px solid var(--ogrid-border);
|
|
106
|
+
padding: 6px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.ogrid-column-menu-btn {
|
|
110
|
+
background: none;
|
|
111
|
+
border: none;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
padding: 4px 6px;
|
|
114
|
+
font-size: 16px;
|
|
115
|
+
color: var(--ogrid-fg-muted);
|
|
116
|
+
line-height: 1;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
display: inline-flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
min-width: 24px;
|
|
123
|
+
height: 24px;
|
|
124
|
+
transition: background-color 0.15s;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ogrid-column-menu-btn:hover {
|
|
128
|
+
background: var(--ogrid-hover-bg, rgba(0, 0, 0, 0.04));
|
|
129
|
+
color: var(--ogrid-fg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* === Checkbox column === */
|
|
133
|
+
|
|
134
|
+
.ogrid-checkbox-header,
|
|
135
|
+
.ogrid-checkbox-cell {
|
|
136
|
+
text-align: center;
|
|
137
|
+
padding: 4px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.ogrid-checkbox-cell {
|
|
141
|
+
padding: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.ogrid-checkbox-wrapper {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ogrid-checkbox-spacer {
|
|
151
|
+
padding: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* === Row numbers === */
|
|
155
|
+
|
|
156
|
+
.ogrid-row-number-header {
|
|
157
|
+
text-align: center;
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
background-color: var(--ogrid-header-bg);
|
|
160
|
+
color: var(--ogrid-fg-secondary);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.ogrid-row-number-spacer {
|
|
164
|
+
background-color: var(--ogrid-header-bg);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.ogrid-row-number-cell {
|
|
168
|
+
text-align: center;
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
font-variant-numeric: tabular-nums;
|
|
171
|
+
background-color: var(--ogrid-header-bg);
|
|
172
|
+
color: var(--ogrid-fg-secondary);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* === Data cells === */
|
|
176
|
+
|
|
177
|
+
.ogrid-data-cell {
|
|
178
|
+
position: relative;
|
|
179
|
+
padding: 0;
|
|
180
|
+
height: 1px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.ogrid-data-cell--pinned-left {
|
|
184
|
+
position: sticky;
|
|
185
|
+
z-index: 6;
|
|
186
|
+
background-color: var(--ogrid-bg);
|
|
187
|
+
will-change: transform;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.ogrid-data-cell--pinned-right {
|
|
191
|
+
position: sticky;
|
|
192
|
+
z-index: 6;
|
|
193
|
+
background-color: var(--ogrid-bg);
|
|
194
|
+
will-change: transform;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.ogrid-cell-content {
|
|
198
|
+
width: 100%;
|
|
199
|
+
height: 100%;
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
min-width: 0;
|
|
203
|
+
padding: 6px 10px;
|
|
204
|
+
box-sizing: border-box;
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
text-overflow: ellipsis;
|
|
207
|
+
white-space: nowrap;
|
|
208
|
+
user-select: none;
|
|
209
|
+
outline: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.ogrid-cell-content--numeric {
|
|
213
|
+
justify-content: flex-end;
|
|
214
|
+
text-align: right;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.ogrid-cell-content--boolean {
|
|
218
|
+
justify-content: center;
|
|
219
|
+
text-align: center;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ogrid-cell-content--editable {
|
|
223
|
+
cursor: cell;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.ogrid-cell-content--active {
|
|
227
|
+
outline: 2px solid var(--ogrid-selection, #217346);
|
|
228
|
+
outline-offset: -1px;
|
|
229
|
+
z-index: 2;
|
|
230
|
+
position: relative;
|
|
231
|
+
overflow: visible;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* === Editing cell wrapper === */
|
|
235
|
+
|
|
236
|
+
.ogrid-editing-cell {
|
|
237
|
+
width: 100%;
|
|
238
|
+
height: 100%;
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
box-sizing: border-box;
|
|
242
|
+
outline: 2px solid var(--ogrid-selection-color, #217346);
|
|
243
|
+
outline-offset: -1px;
|
|
244
|
+
z-index: 2;
|
|
245
|
+
position: relative;
|
|
246
|
+
background: var(--ogrid-bg, #fff);
|
|
247
|
+
overflow: visible;
|
|
248
|
+
padding: 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* === Fill handle === */
|
|
252
|
+
|
|
253
|
+
.ogrid-fill-handle {
|
|
254
|
+
position: absolute;
|
|
255
|
+
right: -3px;
|
|
256
|
+
bottom: -3px;
|
|
257
|
+
width: 7px;
|
|
258
|
+
height: 7px;
|
|
259
|
+
background-color: var(--ogrid-selection, #217346);
|
|
260
|
+
border: 1px solid var(--ogrid-bg);
|
|
261
|
+
border-radius: 1px;
|
|
262
|
+
cursor: crosshair;
|
|
263
|
+
pointer-events: auto;
|
|
264
|
+
z-index: 3;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* === Resize handle === */
|
|
268
|
+
|
|
269
|
+
.ogrid-resize-handle {
|
|
270
|
+
position: absolute;
|
|
271
|
+
top: 0;
|
|
272
|
+
right: -3px;
|
|
273
|
+
bottom: 0;
|
|
274
|
+
width: 8px;
|
|
275
|
+
cursor: col-resize;
|
|
276
|
+
user-select: none;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* === Drop indicator === */
|
|
280
|
+
|
|
281
|
+
.ogrid-drop-indicator {
|
|
282
|
+
position: absolute;
|
|
283
|
+
top: 0;
|
|
284
|
+
bottom: 0;
|
|
285
|
+
width: 3px;
|
|
286
|
+
background: var(--ogrid-primary, #217346);
|
|
287
|
+
pointer-events: none;
|
|
288
|
+
z-index: 100;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* === Empty state === */
|
|
292
|
+
|
|
293
|
+
.ogrid-empty-state {
|
|
294
|
+
padding: 32px 16px;
|
|
295
|
+
text-align: center;
|
|
296
|
+
border-top: 1px solid var(--ogrid-border);
|
|
297
|
+
background-color: var(--ogrid-header-bg);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.ogrid-empty-state-title {
|
|
301
|
+
font-size: 1.25rem;
|
|
302
|
+
font-weight: 600;
|
|
303
|
+
margin-bottom: 8px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.ogrid-empty-state-message {
|
|
307
|
+
font-size: 0.875rem;
|
|
308
|
+
color: var(--ogrid-fg-secondary);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* === Loading overlay === */
|
|
312
|
+
|
|
313
|
+
.ogrid-loading-overlay {
|
|
314
|
+
position: absolute;
|
|
315
|
+
inset: 0;
|
|
316
|
+
z-index: 2;
|
|
317
|
+
display: flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
justify-content: center;
|
|
320
|
+
background-color: var(--ogrid-loading-overlay);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.ogrid-loading-inner {
|
|
324
|
+
display: flex;
|
|
325
|
+
flex-direction: column;
|
|
326
|
+
align-items: center;
|
|
327
|
+
gap: 8px;
|
|
328
|
+
padding: 16px;
|
|
329
|
+
background-color: var(--ogrid-bg);
|
|
330
|
+
border: 1px solid var(--ogrid-border);
|
|
331
|
+
border-radius: 4px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.ogrid-loading-message {
|
|
335
|
+
font-size: 0.875rem;
|
|
336
|
+
color: var(--ogrid-fg-secondary);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* === Popover editor anchor === */
|
|
340
|
+
|
|
341
|
+
.ogrid-popover-anchor {
|
|
342
|
+
min-height: 100%;
|
|
343
|
+
min-width: 40px;
|
|
344
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* OGrid Shared Theme Variables — consumed by vue-vuetify and vue-primevue */
|
|
2
|
+
|
|
3
|
+
/* ─── OGrid Theme Variables ─── */
|
|
4
|
+
:root {
|
|
5
|
+
--ogrid-bg: #ffffff;
|
|
6
|
+
--ogrid-fg: rgba(0, 0, 0, 0.87);
|
|
7
|
+
--ogrid-fg-secondary: rgba(0, 0, 0, 0.6);
|
|
8
|
+
--ogrid-fg-muted: rgba(0, 0, 0, 0.5);
|
|
9
|
+
--ogrid-border: rgba(0, 0, 0, 0.12);
|
|
10
|
+
--ogrid-header-bg: rgba(0, 0, 0, 0.04);
|
|
11
|
+
--ogrid-hover-bg: rgba(0, 0, 0, 0.04);
|
|
12
|
+
--ogrid-selected-row-bg: #e6f0fb;
|
|
13
|
+
--ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);
|
|
14
|
+
--ogrid-range-bg: rgba(33, 115, 70, 0.12);
|
|
15
|
+
--ogrid-accent: #0078d4;
|
|
16
|
+
--ogrid-selection-color: #217346;
|
|
17
|
+
--ogrid-loading-overlay: rgba(255, 255, 255, 0.7);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root:not([data-theme="light"]) {
|
|
22
|
+
--ogrid-bg: #1e1e1e;
|
|
23
|
+
--ogrid-fg: rgba(255, 255, 255, 0.87);
|
|
24
|
+
--ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
|
|
25
|
+
--ogrid-fg-muted: rgba(255, 255, 255, 0.5);
|
|
26
|
+
--ogrid-border: rgba(255, 255, 255, 0.12);
|
|
27
|
+
--ogrid-header-bg: rgba(255, 255, 255, 0.06);
|
|
28
|
+
--ogrid-hover-bg: rgba(255, 255, 255, 0.08);
|
|
29
|
+
--ogrid-selected-row-bg: #1a3a5c;
|
|
30
|
+
--ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
|
|
31
|
+
--ogrid-range-bg: rgba(46, 160, 67, 0.15);
|
|
32
|
+
--ogrid-accent: #4da6ff;
|
|
33
|
+
--ogrid-selection-color: #2ea043;
|
|
34
|
+
--ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-theme="dark"] {
|
|
39
|
+
--ogrid-bg: #1e1e1e;
|
|
40
|
+
--ogrid-fg: rgba(255, 255, 255, 0.87);
|
|
41
|
+
--ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
|
|
42
|
+
--ogrid-fg-muted: rgba(255, 255, 255, 0.5);
|
|
43
|
+
--ogrid-border: rgba(255, 255, 255, 0.12);
|
|
44
|
+
--ogrid-header-bg: rgba(255, 255, 255, 0.06);
|
|
45
|
+
--ogrid-hover-bg: rgba(255, 255, 255, 0.08);
|
|
46
|
+
--ogrid-selected-row-bg: #1a3a5c;
|
|
47
|
+
--ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
|
|
48
|
+
--ogrid-range-bg: rgba(46, 160, 67, 0.15);
|
|
49
|
+
--ogrid-accent: #4da6ff;
|
|
50
|
+
--ogrid-selection-color: #2ea043;
|
|
51
|
+
--ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
|
|
52
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type PropType, type VNode } from 'vue';
|
|
2
|
+
import type { IColumnDef } from '../types';
|
|
3
|
+
export interface CreateInlineCellEditorOptions {
|
|
4
|
+
renderCheckbox: (props: {
|
|
5
|
+
checked: boolean;
|
|
6
|
+
onChange: (val: boolean) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}) => VNode;
|
|
9
|
+
renderDatePicker: (props: {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (val: string) => void;
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
}) => VNode;
|
|
14
|
+
}
|
|
15
|
+
export declare function createInlineCellEditor(options: CreateInlineCellEditorOptions): import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
16
|
+
value: {
|
|
17
|
+
default: undefined;
|
|
18
|
+
};
|
|
19
|
+
item: {
|
|
20
|
+
type: ObjectConstructor;
|
|
21
|
+
required: true;
|
|
22
|
+
};
|
|
23
|
+
column: {
|
|
24
|
+
type: PropType<IColumnDef>;
|
|
25
|
+
required: true;
|
|
26
|
+
};
|
|
27
|
+
rowIndex: {
|
|
28
|
+
type: NumberConstructor;
|
|
29
|
+
required: true;
|
|
30
|
+
};
|
|
31
|
+
editorType: {
|
|
32
|
+
type: PropType<"text" | "select" | "checkbox" | "richSelect" | "date">;
|
|
33
|
+
required: true;
|
|
34
|
+
};
|
|
35
|
+
onCommit: {
|
|
36
|
+
type: PropType<(value: unknown) => void>;
|
|
37
|
+
required: true;
|
|
38
|
+
};
|
|
39
|
+
onCancel: {
|
|
40
|
+
type: PropType<() => void>;
|
|
41
|
+
required: true;
|
|
42
|
+
};
|
|
43
|
+
}>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
46
|
+
value: {
|
|
47
|
+
default: undefined;
|
|
48
|
+
};
|
|
49
|
+
item: {
|
|
50
|
+
type: ObjectConstructor;
|
|
51
|
+
required: true;
|
|
52
|
+
};
|
|
53
|
+
column: {
|
|
54
|
+
type: PropType<IColumnDef>;
|
|
55
|
+
required: true;
|
|
56
|
+
};
|
|
57
|
+
rowIndex: {
|
|
58
|
+
type: NumberConstructor;
|
|
59
|
+
required: true;
|
|
60
|
+
};
|
|
61
|
+
editorType: {
|
|
62
|
+
type: PropType<"text" | "select" | "checkbox" | "richSelect" | "date">;
|
|
63
|
+
required: true;
|
|
64
|
+
};
|
|
65
|
+
onCommit: {
|
|
66
|
+
type: PropType<(value: unknown) => void>;
|
|
67
|
+
required: true;
|
|
68
|
+
};
|
|
69
|
+
onCancel: {
|
|
70
|
+
type: PropType<() => void>;
|
|
71
|
+
required: true;
|
|
72
|
+
};
|
|
73
|
+
}>> & Readonly<{}>, {
|
|
74
|
+
value: undefined;
|
|
75
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OGrid factory for Vue UI packages.
|
|
3
|
+
*
|
|
4
|
+
* Both vue-vuetify and vue-primevue OGrid components are 100% identical —
|
|
5
|
+
* they only differ in which DataGridTable, ColumnChooser, and PaginationControls
|
|
6
|
+
* components they use. This factory extracts all shared logic into one place.
|
|
7
|
+
*/
|
|
8
|
+
import { type PropType, type VNode, type Component } from 'vue';
|
|
9
|
+
import type { IOGridProps } from '../types';
|
|
10
|
+
/** Framework-specific component bindings passed by each UI package */
|
|
11
|
+
export interface IOGridUIBindings {
|
|
12
|
+
/** Package-local DataGridTable component */
|
|
13
|
+
DataGridTable: Component;
|
|
14
|
+
/** Package-local ColumnChooser component */
|
|
15
|
+
ColumnChooser: Component;
|
|
16
|
+
/** Package-local PaginationControls component */
|
|
17
|
+
PaginationControls: Component;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates an OGrid component with framework-specific UI bindings.
|
|
21
|
+
* All orchestration logic, sidebar, toolbar, and layout are shared.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createOGrid(ui: IOGridUIBindings): import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
24
|
+
gridProps: {
|
|
25
|
+
type: PropType<IOGridProps<unknown>>;
|
|
26
|
+
required: true;
|
|
27
|
+
};
|
|
28
|
+
}>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
31
|
+
gridProps: {
|
|
32
|
+
type: PropType<IOGridProps<unknown>>;
|
|
33
|
+
required: true;
|
|
34
|
+
};
|
|
35
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -9,4 +9,6 @@ export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGri
|
|
|
9
9
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
|
|
10
10
|
export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, CellInteractionProps, } from './utils';
|
|
11
11
|
export { createDataGridTable, type IDataGridTableUIBindings } from './components/createDataGridTable';
|
|
12
|
+
export { createInlineCellEditor, type CreateInlineCellEditorOptions } from './components/createInlineCellEditor';
|
|
13
|
+
export { createOGrid, type IOGridUIBindings } from './components/createOGrid';
|
|
12
14
|
export type { SideBarProps, SideBarFilterColumn } from './components/SideBar';
|
|
@@ -77,6 +77,8 @@ interface IOGridBaseProps<T> {
|
|
|
77
77
|
columnReorder?: boolean;
|
|
78
78
|
/** Virtual scrolling configuration. Set `enabled: true` with a fixed `rowHeight` to virtualize large datasets. */
|
|
79
79
|
virtualScroll?: IVirtualScrollConfig;
|
|
80
|
+
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
81
|
+
rowHeight?: number;
|
|
80
82
|
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
81
83
|
density?: 'compact' | 'normal' | 'comfortable';
|
|
82
84
|
'aria-label'?: string;
|
|
@@ -157,6 +159,8 @@ export interface IOGridDataGridProps<T> {
|
|
|
157
159
|
columnReorder?: boolean;
|
|
158
160
|
/** Virtual scrolling configuration. */
|
|
159
161
|
virtualScroll?: IVirtualScrollConfig;
|
|
162
|
+
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
163
|
+
rowHeight?: number;
|
|
160
164
|
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
161
165
|
density?: 'compact' | 'normal' | 'comfortable';
|
|
162
166
|
'aria-label'?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-vue",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.22",
|
|
4
4
|
"description": "OGrid Vue – Vue 3 composables, headless components, and utilities for OGrid data grids.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
"types": "./dist/types/index.d.ts",
|
|
11
11
|
"import": "./dist/esm/index.js",
|
|
12
12
|
"require": "./dist/esm/index.js"
|
|
13
|
-
}
|
|
13
|
+
},
|
|
14
|
+
"./styles/*": "./dist/esm/styles/*"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
|
-
"build": "rimraf dist && tsc -p tsconfig.build.json",
|
|
17
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json && node scripts/copy-css.js",
|
|
17
18
|
"test": "jest --passWithNoTests"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"node": ">=18"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@alaarab/ogrid-core": "2.0.
|
|
39
|
+
"@alaarab/ogrid-core": "2.0.22"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"vue": "^3.3.0"
|
|
@@ -47,7 +48,9 @@
|
|
|
47
48
|
"vite-plugin-dts": "^4.5.4",
|
|
48
49
|
"typescript": "^5.9.3"
|
|
49
50
|
},
|
|
50
|
-
"sideEffects":
|
|
51
|
+
"sideEffects": [
|
|
52
|
+
"**/*.css"
|
|
53
|
+
],
|
|
51
54
|
"publishConfig": {
|
|
52
55
|
"access": "public"
|
|
53
56
|
}
|