@xleddyl/443-components-vue 0.9.2 → 0.10.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/CATALOG.md +331 -0
- package/README.md +73 -16
- package/dist/components/templates/SettingsModal.vue +35 -14
- package/dist/components/templates/SettingsModal.vue.d.ts +2 -0
- package/dist/composables/useForm.d.ts +10 -0
- package/dist/composables/useForm.mjs +63 -2
- package/dist/grid/DataGrid.vue +90 -13
- package/dist/grid/DataGrid.vue.d.ts +44 -2
- package/dist/grid/cells/LinkCell.vue +8 -7
- package/dist/grid/cells/LinkCell.vue.d.ts +1 -417
- package/dist/grid/data-grid.css +113 -0
- package/dist/grid/index.d.ts +2 -1
- package/dist/grid/index.mjs +3 -1
- package/dist/grid/modules.d.ts +1 -0
- package/dist/grid/modules.mjs +8 -0
- package/dist/grid/spreadsheet.d.ts +52 -0
- package/dist/grid/spreadsheet.mjs +928 -0
- package/dist/grid/tsv.d.ts +3 -0
- package/dist/grid/tsv.mjs +88 -0
- package/dist/grid/types.d.ts +81 -0
- package/dist/locale/de.mjs +10 -1
- package/dist/locale/en.mjs +10 -1
- package/dist/locale/it.mjs +10 -1
- package/dist/locale/types.d.ts +9 -0
- package/dist/styles/templates/settings-modal.css +38 -9
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Compact, scoped dashboard components for Vue 3.5 and Nuxt 4. Plain Vue and CSS, no runtime dependency besides `vue`. Every style lives under `.k-root` and inside cascade layers, so the host page is never touched.
|
|
4
4
|
|
|
5
|
+
Looking for the right component for a task? Read [CATALOG.md](./CATALOG.md) first: it lists every component and composable with what it is for and when to use it.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```sh
|
|
@@ -127,21 +129,21 @@ Input tokens, declared only on the outermost root so nested roots inherit them:
|
|
|
127
129
|
|
|
128
130
|
A few component tokens are unset by default and only exist to be overridden on a root (or any ancestor). Unset, every one keeps the library look. The full list with defaults is in [CONVENTIONS.md](./CONVENTIONS.md#tokens):
|
|
129
131
|
|
|
130
|
-
| Area | Tokens
|
|
131
|
-
| -------------- |
|
|
132
|
-
| Type | `--k-font-weight` (400), `--k-font-weight-medium` (500), `--k-font-weight-strong` (600), `--k-font-weight-display` (650)
|
|
133
|
-
| Buttons | `--k-btn-highlight`, `--k-btn-shadow` (`0 0 #0000` for a flat button), `--k-btn-border`, `--k-btn-hover-background`, `--k-btn-active-background`, `--k-btn-padding-x`, `--k-btn-font-size`, `--k-btn-icon-size`, `--k-press-scale` (0.97)
|
|
134
|
-
| Fields | `--k-field-padding-x`
|
|
135
|
-
| Overlays | `--k-modal-radius`, `--k-modal-shadow`, `--k-drawer-radius`, `--k-drawer-shadow`, `--k-popover-radius`, `--k-popover-shadow`, `--k-toast-radius`, `--k-toast-shadow`, `--k-scrim-blur` (3px)
|
|
136
|
-
| Tooltip | `--k-tooltip-background`, `--k-tooltip-text`, `--k-tooltip-border`, `--k-tooltip-radius`, `--k-tooltip-shadow`, `--k-tooltip-padding`
|
|
137
|
-
| Shell | `--k-sidebar-width` (15rem), `--k-sidebar-rail-width`, `--k-topbar-height` (3rem), `--k-shell-inset`, `--k-shell-padding`, `--k-shell-content`
|
|
138
|
-
| Switch | `--k-switch-track-width`, `--k-switch-track-height`, `--k-switch-thumb-size` (md size)
|
|
139
|
-
| Separator | `--k-separator-heading-font-size` (0.9375rem), `--k-separator-heading-color` (`--k-text`), `--k-separator-label-font-size` (0.75rem)
|
|
140
|
-
| Calendar | `--k-calendar-day-radius`, `--k-calendar-unavailable-color`, `--k-calendar-unavailable-decoration` (line-through), `--k-calendar-unavailable-opacity` (0.6), `--k-calendar-out-of-range-color`, `--k-calendar-out-of-range-decoration`, `--k-calendar-out-of-range-opacity` (each defaults to its unavailable token)
|
|
141
|
-
| Checkbox group | `--k-checkbox-group-background`, `--k-checkbox-group-padding` (0.75rem), `--k-checkbox-group-radius`, `--k-checkbox-group-border` (`variant="filled"`)
|
|
142
|
-
| Badge | `--k-badge-radius`, `--k-badge-font`, `--k-badge-font-size`, `--k-badge-font-weight`, `--k-badge-text-transform`, `--k-badge-letter-spacing`
|
|
143
|
-
| DataGrid | `--k-grid-header-background`, `--k-grid-header-text`, `--k-grid-header-font`, `--k-grid-header-font-weight`, `--k-grid-header-font-size` (12px), `--k-grid-font-size` (13px), `--k-grid-radius`, `--k-grid-selected-background`
|
|
144
|
-
| Charts | `--k-chart-1` to `--k-chart-8` (series palette)
|
|
132
|
+
| Area | Tokens |
|
|
133
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| Type | `--k-font-weight` (400), `--k-font-weight-medium` (500), `--k-font-weight-strong` (600), `--k-font-weight-display` (650) |
|
|
135
|
+
| Buttons | `--k-btn-highlight`, `--k-btn-shadow` (`0 0 #0000` for a flat button), `--k-btn-border`, `--k-btn-hover-background`, `--k-btn-active-background`, `--k-btn-padding-x`, `--k-btn-font-size`, `--k-btn-icon-size`, `--k-press-scale` (0.97) |
|
|
136
|
+
| Fields | `--k-field-padding-x` |
|
|
137
|
+
| Overlays | `--k-modal-radius`, `--k-modal-shadow`, `--k-drawer-radius`, `--k-drawer-shadow`, `--k-popover-radius`, `--k-popover-shadow`, `--k-toast-radius`, `--k-toast-shadow`, `--k-scrim-blur` (3px) |
|
|
138
|
+
| Tooltip | `--k-tooltip-background`, `--k-tooltip-text`, `--k-tooltip-border`, `--k-tooltip-radius`, `--k-tooltip-shadow`, `--k-tooltip-padding` |
|
|
139
|
+
| Shell | `--k-sidebar-width` (15rem), `--k-sidebar-rail-width`, `--k-topbar-height` (3rem), `--k-shell-inset`, `--k-shell-padding`, `--k-shell-content` |
|
|
140
|
+
| Switch | `--k-switch-track-width`, `--k-switch-track-height`, `--k-switch-thumb-size` (md size) |
|
|
141
|
+
| Separator | `--k-separator-heading-font-size` (0.9375rem), `--k-separator-heading-color` (`--k-text`), `--k-separator-label-font-size` (0.75rem) |
|
|
142
|
+
| Calendar | `--k-calendar-day-radius`, `--k-calendar-unavailable-color`, `--k-calendar-unavailable-decoration` (line-through), `--k-calendar-unavailable-opacity` (0.6), `--k-calendar-out-of-range-color`, `--k-calendar-out-of-range-decoration`, `--k-calendar-out-of-range-opacity` (each defaults to its unavailable token) |
|
|
143
|
+
| Checkbox group | `--k-checkbox-group-background`, `--k-checkbox-group-padding` (0.75rem), `--k-checkbox-group-radius`, `--k-checkbox-group-border` (`variant="filled"`) |
|
|
144
|
+
| Badge | `--k-badge-radius`, `--k-badge-font`, `--k-badge-font-size`, `--k-badge-font-weight`, `--k-badge-text-transform`, `--k-badge-letter-spacing` |
|
|
145
|
+
| DataGrid | `--k-grid-header-background`, `--k-grid-header-text`, `--k-grid-header-font`, `--k-grid-header-font-weight`, `--k-grid-header-font-size` (12px), `--k-grid-font-size` (13px), `--k-grid-radius`, `--k-grid-selected-background`, spreadsheet mode: `--k-grid-range-background`, `--k-grid-range-border`, `--k-grid-dirty-background`, `--k-grid-dirty-marker` |
|
|
146
|
+
| Charts | `--k-chart-1` to `--k-chart-8` (series palette) |
|
|
145
147
|
|
|
146
148
|
Some tokens are meant for one element: set `--k-btn-tone`, `--k-btn-on-tone` and `--k-btn-tone-text` on a button for a brand tone outside `Tone` (a lime fill with green text), and `--k-badge-background`, `--k-badge-color` and `--k-badge-border-color` on a badge for a custom pill color. `variant="subtle"` is a borderless tinted fill (`--k-fill` for the neutral tone). For solid buttons without hover darkening, declare the fill tokens on the button so `var(--k-btn-tone)` resolves per tone: `.k-btn-solid { --k-btn-hover-background: var(--k-btn-tone); --k-btn-active-background: var(--k-btn-tone) }` (the neutral solid button rests on `--k-text`).
|
|
147
149
|
|
|
@@ -263,6 +265,28 @@ const form = useForm({
|
|
|
263
265
|
|
|
264
266
|
Issues are keyed by their first path segment and by their full dotted path (`blocks.0.title`), so a nested `FormField name="blocks.0.title"` shows its own error. Pass `issueKey: (path) => string | string[]` to choose the keys yourself. Issues with an empty path become `rootErrors`, which `Form` renders as alerts above the content (`alerts="end"` moves them below, `alerts="false"` hides them, the `#alerts` slot replaces them). `setError('', message)` adds a root error, for example from the server.
|
|
265
267
|
|
|
268
|
+
Server errors. `form.setIssues(issues)` takes Standard Schema shaped issues (`FormIssueInput`, `{ message, path? }`), keys them like validator issues (first segment and dotted path) and shows them on their fields; an empty path becomes a root error. Messages go through `translateMessage` (`kind: 'error'`). When `onSubmit` throws, a `submitError` resolver (`FormSubmitErrorResolver`, `(error) => FormSubmitError | null | undefined`, with `FormSubmitError` `{ message?, issues? }`) turns the error into issues: `submit()` then resolves `false` instead of throwing, `Form` focuses the first invalid field (or the alerts), `form.submitError` holds the mapped result, and `message` becomes a root error when no issue landed on a field. Set the resolver once in `createFormConfig` / `provideFormConfig`, or per form in `useForm({ submitError })`, which wins. Without a resolver, or when it returns nothing (or neither a message nor issues), the error is thrown as before. Server errors are stale once the user edits: changing a field drops the server errors of that field and its nested paths, and the next submit drops the rest. `setError` keeps its behavior: its errors stay until the field validates again (after the first submit every edit validates the field, so they clear on edit too; before it they stay).
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
app.use(
|
|
272
|
+
createFormConfig({
|
|
273
|
+
submitError: (error) => {
|
|
274
|
+
const data = (
|
|
275
|
+
error as { data?: { message?: string; data?: { issues?: FormIssueInput[] } } }
|
|
276
|
+
).data
|
|
277
|
+
return data ? { message: data.message, issues: data.data?.issues } : null
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const form = useForm({
|
|
283
|
+
values: product,
|
|
284
|
+
onSubmit: (values) => $fetch('/api/products', { method: 'POST', body: values }),
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The server answers `422` with `{ message: 'Invalid product', data: { issues: [{ message: 'Taken', path: ['sku'] }] } }` (`createError({ statusCode: 422, message, data: { issues } })` in Nitro), and `$fetch` puts that body on `error.data`.
|
|
289
|
+
|
|
266
290
|
Built-in messages follow the active language. `messages` overrides them for one form.
|
|
267
291
|
|
|
268
292
|
`schema` also accepts a ref or getter (`MaybeRefOrGetter<FormSchema>`), for one form component reused with different fields, labels or rules per open, mode or language: `schema: () => (mode.value === 'edit' ? editSchema : createSchema)`. Field definitions, labels, `required` and rules follow the source (after the first submit the form validates again when they change). A field that appears starts from its `initial` and is clean; a field that disappears loses its value, error and touched state. With `values` the host owns the values, so only rules and labels change. `form.schema` always returns the current definitions.
|
|
@@ -509,7 +533,7 @@ A large dialog (60rem) with a section nav on the left and the content on the rig
|
|
|
509
533
|
- Built-in sections, used when no `#section-<id>` slot fills them: `account` (profile card, your `#account-fields`, a sign out button) and `appearance` (`AppearanceSettings`: theme cards light, dark, system and preset cards; `presets` limits or hides them).
|
|
510
534
|
- `@sign-out` adds a Sign out item at the bottom of the nav and in the account section.
|
|
511
535
|
- Labels: `title` (accessible name of the dialog, default `Settings`), `closeLabel`, `navLabel`, `signOutLabel`, `accountLabel`, `accountText`, `appearanceLabel`, `appearanceText`, `sessionLabel`, `sessionText`, `themeLabel`, `themeText`, `presetLabel`, `presetText`.
|
|
512
|
-
- Subpages: `#section-<id>` and `#actions-<id>` get `page`, `pages`, `openPage({ id, label })` and `back()`. With a page open,
|
|
536
|
+
- Subpages: `#section-<id>` and `#actions-<id>` get `page`, `pages`, `openPage({ id, label })` and `back()`. With a page open, the header title becomes the path (`Members > Grace Hopper`, same size, only the last part in the text color, earlier parts are buttons that go back to them), the section description stays, and a Back button (`backLabel`, default `common.back`) shows on the right before `#actions-<id>`. `v-model:pages` binds the stack (it also works unbound). Changing section clears it. Render the detail when `page` is set, the list otherwise.
|
|
513
537
|
- `hide-session` hides the Session group (sign out) in the account section; the Sign out item in the nav stays. `beforeClose` works as on Modal.
|
|
514
538
|
- Accessibility: native modal `<dialog>` (focus trap, Escape, focus returns to the trigger), the nav is a labelled `nav` of buttons with `aria-current` and `aria-controls`, the content is a `section` labelled by its heading.
|
|
515
539
|
|
|
@@ -666,6 +690,39 @@ Accessibility related props and defaults:
|
|
|
666
690
|
- `DataGrid` registers the AG Grid cell editors (text, large text, number, date, checkbox, select, custom and undo/redo) when `editable` is set or when a column or `gridOptions.defaultColDef` has `editable` at mount. `registerGridEditorModules()` from `/grid` does it by hand, before the first grid mounts, for columns that become editable later.
|
|
667
691
|
- `ImageCell` takes `fit` (`'cover'` default, or `'contain'` for logos) and `size` (any CSS length, default `1.75rem`) in `cellRendererParams`. `EmptyCell` (the hidden "Empty" dash) is exported from `/grid` for custom renderers.
|
|
668
692
|
- `BadgeCell` takes `variant` (`BadgeVariant`, `'soft' | 'solid' | 'outline'`) in `cellRendererParams`: set, the cell renders a `KBadge` with that variant and the resolved tone (`size`, `BadgeSize`, default `'sm'`); unset keeps the compact grid pill. `badgeClass` (`string | ((value, data) => string | undefined)`) adds host classes to the badge, to set the per instance badge tokens (`--k-badge-background`, `--k-badge-color`, `--k-badge-border-color`, which the grid pill also reads) for values outside `Tone`. `BadgeCellParams<T>` types the row of `data`.
|
|
693
|
+
- `DataGrid` `spreadsheet` turns on Excel like editing on AG Grid Community (no Enterprise modules): it registers the editor modules and the AG Grid `ScrollApiModule` (`registerGridSpreadsheetModules()` from `/grid` does it by hand) and leaves the normal grid unchanged when it is off. Set `rowIdKey` or `getRowId`: changes, dirty cells and undo refer to row ids, never to indexes.
|
|
694
|
+
- Selection: click selects a cell; drag, Shift+click and Shift+arrows extend a rectangular range (Shift+Cmd/Ctrl+arrow to the edge); Cmd/Ctrl+A selects every cell. The range follows the displayed row order (after sort, filter and quick search) and the displayed column order; the row selection checkbox column is not part of it. Escape collapses the range to the active cell.
|
|
695
|
+
- Keyboard: arrows and Tab move the active cell, Enter, F2 or typing start the AG Grid editor, Escape cancels it. While an editor (or any input) has focus no shortcut fires.
|
|
696
|
+
- Copy: Cmd/Ctrl+C copies the range as TSV (tabs between cells, newlines between rows, cells with a tab, a newline or a leading quote are quoted like Excel). Cells copy their raw value as text (`true`/`false`, dates as `YYYY-MM-DD`); `gridOptions.processCellForClipboard` changes it (it gets `formatValue` and `parseValue` helpers). Right click inside a range copies the range.
|
|
697
|
+
- Paste: Cmd/Ctrl+V reads TSV from Excel, Google Sheets or text (quoted multi line cells, CRLF, trailing newline) and starts at the top left cell of the range. One value fills the whole range; a smaller block that divides the range evenly is tiled; otherwise the block keeps its size and the pasted area becomes the range. Read only cells (`editable` false or a function that returns false) are skipped. Text goes through the column `valueParser` (also the one of `cellDataType`) and then `valueSetter` or `field`; `gridOptions.processCellFromClipboard` replaces the parser. A non empty text that parses to `null`, `undefined`, `NaN` or an invalid date, or a value missing from `cellEditorParams.values` of a select editor, is not written: it is listed in `cellsInvalid` and counted in the toast.
|
|
698
|
+
- Fill: Cmd/Ctrl+D copies the first row of the range down, Cmd/Ctrl+R the first column to the right (a one row or one column range copies from the row above or the column on the left). Drag the small square on the range corner down to repeat the range rows below it. Delete or Backspace clears the editable cells (`null`, or what the column `valueParser` returns for an empty string).
|
|
699
|
+
- Changes: every edit, paste, fill, clear, bulk set, undo and redo is one batch, emitted as `cellsChange` with `{ changes: GridCellChange[], source }` (`GridCellChange` is `{ rowId, colId, field, oldValue, newValue }`, `source` is `'edit' | 'paste' | 'fill' | 'clear' | 'bulk' | 'undo' | 'redo'`). Cmd/Ctrl+Z undoes a batch, Shift+Cmd/Ctrl+Z and Ctrl+Y redo it (100 batches; AG Grid `undoRedoCellEditing` is turned off). The grid writes into the row objects like AG Grid editing does; persist the batch yourself.
|
|
700
|
+
- Dirty cells: a changed cell gets a tint and a corner marker until you clear it; a cell edited back to its first value is clean again. `dirtyChange(count)` fires when the count changes; `getDirtyCells()` returns `{ rowId, colId, field, originalValue }[]` and `clearDirty(rowIds?)` clears all of them or the given rows (after a save).
|
|
701
|
+
- Events: `cellsChange`, `cellsInvalid` (`{ cells: { rowId, colId, field, text }[], source }`), `dirtyChange`, `rangeChange` (`GridCellRange | null`, `{ startRowIndex, endRowIndex, columns, active: { rowIndex, colId } }`), and `copy(text)` for a copied range.
|
|
702
|
+
- Methods (template ref, typed as `GridSpreadsheetMethods<T>`): `getRange()`, `getSelectedCells()` (`{ rowId, rowIndex, colId, field, value, data, editable }[]`), `selectRange({ startRowIndex, endRowIndex?, startColumn, endColumn? })`, `selectAll()`, `clearRange()`, `setRangeValue(value, { parse? })` (writes the value in every editable cell of the range; `parse: true` runs a string through the paste pipeline), `copyRange()`, `paste(text)`, `fillDown()`, `fillRight()`, `clearCells()`, `undo()`, `redo()`, `clearHistory()`, `getDirtyCells()`, `clearDirty(rowIds?)`. Write methods return the number of changed cells.
|
|
703
|
+
- The `actions` slot also gets every method plus `range`, `selectedCellCount`, `dirtyCount`, `canUndo` and `canRedo` (`GridSpreadsheetSlotProps<T>`), for a "set this value on the selected cells" action:
|
|
704
|
+
|
|
705
|
+
```vue
|
|
706
|
+
<KDataGrid
|
|
707
|
+
ref="grid"
|
|
708
|
+
:rows="products"
|
|
709
|
+
:columns="columns"
|
|
710
|
+
row-id-key="id"
|
|
711
|
+
spreadsheet
|
|
712
|
+
toolbar
|
|
713
|
+
@cells-change="({ changes }) => queue(changes)"
|
|
714
|
+
@dirty-change="(count) => (dirty = count)"
|
|
715
|
+
>
|
|
716
|
+
<template #actions="{ selectedCellCount, setRangeValue, canUndo, undo }">
|
|
717
|
+
<KButton label="Undo" :disabled="!canUndo" @click="undo()" />
|
|
718
|
+
<KButton :label="`Set to Shoes (${selectedCellCount})`" @click="setRangeValue('Shoes')" />
|
|
719
|
+
</template>
|
|
720
|
+
</KDataGrid>
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
- Rendering stays virtual: the range, active cell and dirty marks are `cellClassRules` (merged with your own) and only the rendered cells of the affected rows refresh. The range tint is `--k-grid-range-background`, its border and the fill handle `--k-grid-range-border`, dirty cells `--k-grid-dirty-background` and `--k-grid-dirty-marker`. `parseTsv` and `formatTsv` are exported from `/grid`.
|
|
724
|
+
- `DataGrid` row classes: `gridOptions.rowClass`, `gridOptions.rowClassRules` (`{ 'is-archived': (params) => params.data?.archived }`) and `gridOptions.getRowClass` reach AG Grid unchanged. Style the classes in host CSS under `.k-data-grid .ag-row.<class>`.
|
|
725
|
+
- `LinkCell` renders internal hrefs (starting with `/`) with the configured link component (`NuxtLink` in Nuxt, `createLinkComponent(RouterLink)` in plain Vue), so they navigate without a reload; other hrefs stay a plain `<a>`.
|
|
669
726
|
- `DataGrid` translates the AG Grid ARIA strings through the catalog (`grid.*`; your own `localeText` still wins) and announces row count changes.
|
|
670
727
|
|
|
671
728
|
## Browser support
|
|
@@ -99,21 +99,44 @@
|
|
|
99
99
|
<header class="k-settings-modal-header">
|
|
100
100
|
<div class="k-settings-modal-heading">
|
|
101
101
|
<h2 :id="`${baseId}-title`" class="k-settings-modal-title">
|
|
102
|
-
|
|
102
|
+
<template v-if="currentPage">
|
|
103
|
+
<template v-for="(crumb, index) in trail" :key="index">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="k-settings-modal-crumb"
|
|
107
|
+
@click="goTo(index)"
|
|
108
|
+
>
|
|
109
|
+
{{ crumb }}
|
|
110
|
+
</button>
|
|
111
|
+
<Icon name="chevron-right" class="k-settings-modal-crumb-separator" />
|
|
112
|
+
</template>
|
|
113
|
+
<span class="k-settings-modal-crumb is-current">
|
|
114
|
+
{{ currentPage.label }}
|
|
115
|
+
</span>
|
|
116
|
+
</template>
|
|
117
|
+
<template v-else>{{ active.label }}</template>
|
|
103
118
|
</h2>
|
|
104
119
|
<p v-if="active.description" class="k-settings-modal-description">
|
|
105
120
|
{{ active.description }}
|
|
106
121
|
</p>
|
|
107
122
|
</div>
|
|
108
|
-
<div
|
|
123
|
+
<div
|
|
124
|
+
v-if="currentPage || $slots[`actions-${active.id}`]"
|
|
125
|
+
class="k-settings-modal-actions"
|
|
126
|
+
>
|
|
127
|
+
<Button
|
|
128
|
+
v-if="currentPage"
|
|
129
|
+
variant="ghost"
|
|
130
|
+
tone="neutral"
|
|
131
|
+
size="sm"
|
|
132
|
+
icon="arrow-left"
|
|
133
|
+
@click="back"
|
|
134
|
+
>
|
|
135
|
+
{{ backText }}
|
|
136
|
+
</Button>
|
|
109
137
|
<slot :name="`actions-${active.id}`" v-bind="slotProps(active, close)" />
|
|
110
138
|
</div>
|
|
111
139
|
</header>
|
|
112
|
-
<Breadcrumbs
|
|
113
|
-
v-if="currentPage"
|
|
114
|
-
class="k-settings-modal-breadcrumbs"
|
|
115
|
-
:items="breadcrumbItems"
|
|
116
|
-
/>
|
|
117
140
|
<div
|
|
118
141
|
ref="contentBody"
|
|
119
142
|
class="k-settings-modal-body"
|
|
@@ -186,7 +209,6 @@ import Icon from "../core/Icon.vue";
|
|
|
186
209
|
import Avatar from "../data/Avatar.vue";
|
|
187
210
|
import Badge from "../data/Badge.vue";
|
|
188
211
|
import Modal from "../overlays/Modal.vue";
|
|
189
|
-
import Breadcrumbs from "../layout/Breadcrumbs.vue";
|
|
190
212
|
import AppearanceSettings from "./AppearanceSettings.vue";
|
|
191
213
|
import SettingsGroup from "./SettingsGroup.vue";
|
|
192
214
|
const props = defineProps({
|
|
@@ -195,6 +217,7 @@ const props = defineProps({
|
|
|
195
217
|
presets: { type: [Array, Boolean], required: false, default: () => THEME_PRESETS },
|
|
196
218
|
title: { type: String, required: false },
|
|
197
219
|
closeLabel: { type: String, required: false },
|
|
220
|
+
backLabel: { type: String, required: false },
|
|
198
221
|
navLabel: { type: String, required: false },
|
|
199
222
|
signOutLabel: { type: String, required: false },
|
|
200
223
|
sessionLabel: { type: String, required: false },
|
|
@@ -219,6 +242,7 @@ const pages = defineModel("pages", { type: Array, ...{ default: () => [] } });
|
|
|
219
242
|
const { t } = useLocale();
|
|
220
243
|
const titleText = computed(() => props.title ?? t("settings.title"));
|
|
221
244
|
const closeText = computed(() => props.closeLabel ?? t("settings.close"));
|
|
245
|
+
const backText = computed(() => props.backLabel ?? t("common.back"));
|
|
222
246
|
const navText = computed(() => props.navLabel ?? t("settings.sections"));
|
|
223
247
|
const signOutText = computed(() => props.signOutLabel ?? t("settings.signOut"));
|
|
224
248
|
const sessionLabelText = computed(() => props.sessionLabel ?? t("settings.session"));
|
|
@@ -276,12 +300,9 @@ function openPage(page) {
|
|
|
276
300
|
function back() {
|
|
277
301
|
goTo(pages.value.length - 1);
|
|
278
302
|
}
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
...pages.value.map((page
|
|
282
|
-
label: page.label,
|
|
283
|
-
onClick: index < pages.value.length - 1 ? () => goTo(index + 1) : void 0
|
|
284
|
-
}))
|
|
303
|
+
const trail = computed(() => [
|
|
304
|
+
active.value?.label ?? "",
|
|
305
|
+
...pages.value.slice(0, -1).map((page) => page.label)
|
|
285
306
|
]);
|
|
286
307
|
function slotProps(item, close) {
|
|
287
308
|
return { section: item, close, page: currentPage.value, pages: pages.value, openPage, back };
|
|
@@ -16,6 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
16
16
|
presets?: readonly ThemePreset[] | false;
|
|
17
17
|
title?: string | undefined;
|
|
18
18
|
closeLabel?: string;
|
|
19
|
+
backLabel?: string;
|
|
19
20
|
navLabel?: string;
|
|
20
21
|
signOutLabel?: string;
|
|
21
22
|
sessionLabel?: string;
|
|
@@ -257,6 +258,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
257
258
|
presets?: readonly ThemePreset[] | false;
|
|
258
259
|
title?: string | undefined;
|
|
259
260
|
closeLabel?: string;
|
|
261
|
+
backLabel?: string;
|
|
260
262
|
navLabel?: string;
|
|
261
263
|
signOutLabel?: string;
|
|
262
264
|
sessionLabel?: string;
|
|
@@ -50,9 +50,16 @@ export interface FormTranslateContext {
|
|
|
50
50
|
params?: MessageParams;
|
|
51
51
|
}
|
|
52
52
|
export type FormTranslateMessage = (message: string, context: FormTranslateContext) => string;
|
|
53
|
+
export type FormIssueInput = StandardSchemaIssue;
|
|
54
|
+
export interface FormSubmitError {
|
|
55
|
+
message?: string;
|
|
56
|
+
issues?: ReadonlyArray<FormIssueInput>;
|
|
57
|
+
}
|
|
58
|
+
export type FormSubmitErrorResolver = (error: unknown) => FormSubmitError | undefined | null;
|
|
53
59
|
export interface FormConfig {
|
|
54
60
|
messages?: Partial<FormMessages>;
|
|
55
61
|
translateMessage?: FormTranslateMessage;
|
|
62
|
+
submitError?: FormSubmitErrorResolver;
|
|
56
63
|
}
|
|
57
64
|
export type FormValidationTrigger = 'submit' | 'blur' | 'input';
|
|
58
65
|
export type FormIssueKey = IssueKeyResolver;
|
|
@@ -72,6 +79,7 @@ export interface UseFormOptions<S extends FormSchema = FormSchema, V extends obj
|
|
|
72
79
|
validateOn?: FormValidationTrigger;
|
|
73
80
|
onSubmit?: (values: V) => unknown;
|
|
74
81
|
onInvalid?: (errors: FormErrors<S>) => unknown;
|
|
82
|
+
submitError?: FormSubmitErrorResolver;
|
|
75
83
|
}
|
|
76
84
|
export type FormErrors<S extends FormSchema = FormSchema> = Partial<Record<keyof S & string, string>> & FormMessageMap;
|
|
77
85
|
export interface FormContext {
|
|
@@ -105,6 +113,8 @@ export interface UseFormReturn<S extends FormSchema = FormSchema, V extends obje
|
|
|
105
113
|
validateField: (name: keyof V & string) => Promise<boolean>;
|
|
106
114
|
setError: (name: FormPath<V>, message: string | undefined) => void;
|
|
107
115
|
setErrors: (errors: FormErrors<S>) => void;
|
|
116
|
+
setIssues: (issues: ReadonlyArray<FormIssueInput>) => void;
|
|
117
|
+
submitError: Readonly<Ref<FormSubmitError | null>>;
|
|
108
118
|
clearErrors: () => void;
|
|
109
119
|
touch: (name: string) => void;
|
|
110
120
|
reset: (values?: Partial<V>) => void;
|
|
@@ -146,11 +146,14 @@ export function useForm(options) {
|
|
|
146
146
|
const touched = reactive({});
|
|
147
147
|
const submitCount = ref(0);
|
|
148
148
|
const submitting = ref(false);
|
|
149
|
+
const submitError = shallowRef(null);
|
|
149
150
|
const ruleErrors = {};
|
|
150
151
|
const manualErrors = {};
|
|
151
152
|
let issueErrors = {};
|
|
152
153
|
let issueRoot = [];
|
|
153
154
|
let manualRoot = [];
|
|
155
|
+
const serverKeys = /* @__PURE__ */ new Set();
|
|
156
|
+
let serverRoot = [];
|
|
154
157
|
const readValue = (name) => target()[name];
|
|
155
158
|
const dirtyFields = computed(() => {
|
|
156
159
|
void baselineVersion.value;
|
|
@@ -172,7 +175,46 @@ export function useForm(options) {
|
|
|
172
175
|
}
|
|
173
176
|
function syncErrors(inScope) {
|
|
174
177
|
replaceEntries(errors, composeErrors(), inScope);
|
|
175
|
-
if (!inScope)
|
|
178
|
+
if (!inScope) syncRootErrors();
|
|
179
|
+
}
|
|
180
|
+
function syncRootErrors() {
|
|
181
|
+
rootErrors.value = uniqueMessages(manualRoot, serverRoot, issueRoot);
|
|
182
|
+
}
|
|
183
|
+
function dropServerErrors(inScope = () => true) {
|
|
184
|
+
let dropped = false;
|
|
185
|
+
for (const key of serverKeys) {
|
|
186
|
+
if (!inScope(key)) continue;
|
|
187
|
+
delete manualErrors[key];
|
|
188
|
+
serverKeys.delete(key);
|
|
189
|
+
dropped = true;
|
|
190
|
+
}
|
|
191
|
+
return dropped;
|
|
192
|
+
}
|
|
193
|
+
function applyIssues(issues) {
|
|
194
|
+
dropServerErrors();
|
|
195
|
+
const grouped = groupIssues(translateIssues(schemaIssues(issues), "error"), issueKey);
|
|
196
|
+
for (const [key, message] of Object.entries(grouped.fields)) {
|
|
197
|
+
manualErrors[key] = message;
|
|
198
|
+
serverKeys.add(key);
|
|
199
|
+
}
|
|
200
|
+
serverRoot = grouped.root;
|
|
201
|
+
return grouped;
|
|
202
|
+
}
|
|
203
|
+
function setIssues(issues) {
|
|
204
|
+
applyIssues(issues);
|
|
205
|
+
syncErrors();
|
|
206
|
+
}
|
|
207
|
+
function resolveSubmitError(error) {
|
|
208
|
+
const resolver = options.submitError ?? config?.submitError;
|
|
209
|
+
const result = resolver?.(error);
|
|
210
|
+
return result && (result.message || result.issues?.length) ? result : null;
|
|
211
|
+
}
|
|
212
|
+
function applySubmitError(result) {
|
|
213
|
+
const grouped = applyIssues(result.issues ?? []);
|
|
214
|
+
if (result.message && !Object.keys(grouped.fields).length)
|
|
215
|
+
serverRoot = uniqueMessages(serverRoot, [translate(result.message, { kind: "error" })]);
|
|
216
|
+
submitError.value = result;
|
|
217
|
+
syncErrors();
|
|
176
218
|
}
|
|
177
219
|
function evaluateValidator() {
|
|
178
220
|
const validator = toValue(options.validator);
|
|
@@ -274,6 +316,8 @@ export function useForm(options) {
|
|
|
274
316
|
fields.forEach((name, index) => ruleErrors[name] = results[index]);
|
|
275
317
|
for (const key of Object.keys(manualErrors)) delete manualErrors[key];
|
|
276
318
|
manualRoot = [];
|
|
319
|
+
serverKeys.clear();
|
|
320
|
+
serverRoot = [];
|
|
277
321
|
syncErrors();
|
|
278
322
|
}
|
|
279
323
|
function isValidResult(issues, results) {
|
|
@@ -309,9 +353,10 @@ export function useForm(options) {
|
|
|
309
353
|
function setError(name, message) {
|
|
310
354
|
if (name === "") {
|
|
311
355
|
manualRoot = message ? [message] : [];
|
|
312
|
-
|
|
356
|
+
syncRootErrors();
|
|
313
357
|
return;
|
|
314
358
|
}
|
|
359
|
+
serverKeys.delete(name);
|
|
315
360
|
manualErrors[name] = message;
|
|
316
361
|
if (!message) {
|
|
317
362
|
delete ruleErrors[name];
|
|
@@ -323,6 +368,8 @@ export function useForm(options) {
|
|
|
323
368
|
for (const key of Object.keys(manualErrors)) delete manualErrors[key];
|
|
324
369
|
for (const key of Object.keys(ruleErrors)) delete ruleErrors[key];
|
|
325
370
|
issueErrors = {};
|
|
371
|
+
serverKeys.clear();
|
|
372
|
+
serverRoot = [];
|
|
326
373
|
for (const [key, message] of Object.entries(next)) {
|
|
327
374
|
if (key === "") manualRoot = message ? [message] : [];
|
|
328
375
|
else manualErrors[key] = message;
|
|
@@ -335,6 +382,9 @@ export function useForm(options) {
|
|
|
335
382
|
issueErrors = {};
|
|
336
383
|
issueRoot = [];
|
|
337
384
|
manualRoot = [];
|
|
385
|
+
serverKeys.clear();
|
|
386
|
+
serverRoot = [];
|
|
387
|
+
submitError.value = null;
|
|
338
388
|
syncErrors();
|
|
339
389
|
}
|
|
340
390
|
function touch(name) {
|
|
@@ -374,6 +424,7 @@ export function useForm(options) {
|
|
|
374
424
|
async function submit() {
|
|
375
425
|
if (submitting.value) return false;
|
|
376
426
|
submitCount.value++;
|
|
427
|
+
submitError.value = null;
|
|
377
428
|
const ok = await validate();
|
|
378
429
|
if (!ok) {
|
|
379
430
|
await options.onInvalid?.({ ...errors });
|
|
@@ -382,6 +433,11 @@ export function useForm(options) {
|
|
|
382
433
|
submitting.value = true;
|
|
383
434
|
try {
|
|
384
435
|
await options.onSubmit?.(cloneValue(target()));
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const result = resolveSubmitError(error);
|
|
438
|
+
if (!result) throw error;
|
|
439
|
+
applySubmitError(result);
|
|
440
|
+
return false;
|
|
385
441
|
} finally {
|
|
386
442
|
submitting.value = false;
|
|
387
443
|
}
|
|
@@ -394,6 +450,8 @@ export function useForm(options) {
|
|
|
394
450
|
const current = readValue(name);
|
|
395
451
|
if (snapshots.has(name) && isEqualValue(current, snapshots.get(name))) continue;
|
|
396
452
|
snapshots.set(name, cloneValue(current));
|
|
453
|
+
const inField = (key) => isWithin(key, name);
|
|
454
|
+
if (dropServerErrors(inField)) syncErrors(inField);
|
|
397
455
|
if (validateOn === "input" || submitCount.value > 0 || validateOn === "blur" && touched[name])
|
|
398
456
|
void validateField(name);
|
|
399
457
|
}
|
|
@@ -406,6 +464,7 @@ export function useForm(options) {
|
|
|
406
464
|
if (isWithin(key, name)) delete source[key];
|
|
407
465
|
}
|
|
408
466
|
}
|
|
467
|
+
dropServerErrors((key) => isWithin(key, name));
|
|
409
468
|
}
|
|
410
469
|
watch(
|
|
411
470
|
schemaNames,
|
|
@@ -500,6 +559,8 @@ export function useForm(options) {
|
|
|
500
559
|
validateField,
|
|
501
560
|
setError,
|
|
502
561
|
setErrors,
|
|
562
|
+
setIssues,
|
|
563
|
+
submitError: readonly(submitError),
|
|
503
564
|
clearErrors,
|
|
504
565
|
touch,
|
|
505
566
|
isRequired: isFieldRequired,
|
package/dist/grid/DataGrid.vue
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
class="k-data-grid"
|
|
4
4
|
:class="[
|
|
5
5
|
`k-data-grid-${density}`,
|
|
6
|
-
{
|
|
6
|
+
{
|
|
7
|
+
'is-loading': loading,
|
|
8
|
+
'is-auto-height': autoHeight,
|
|
9
|
+
'is-fill': fill && !autoHeight,
|
|
10
|
+
'is-spreadsheet': spreadsheet,
|
|
11
|
+
'is-filling': spreadsheet && sheetFilling
|
|
12
|
+
}
|
|
7
13
|
]"
|
|
8
14
|
>
|
|
9
15
|
<GridToolbar
|
|
@@ -18,15 +24,20 @@
|
|
|
18
24
|
:debounce="searchDebounce"
|
|
19
25
|
@reset-columns="clearStoredState"
|
|
20
26
|
>
|
|
21
|
-
<slot name="actions" :api="api" />
|
|
27
|
+
<slot name="actions" :api="api" v-bind="sheetSlot" />
|
|
22
28
|
</GridToolbar>
|
|
23
29
|
|
|
24
30
|
<div
|
|
31
|
+
ref="bodyElement"
|
|
25
32
|
class="k-data-grid-body"
|
|
26
33
|
role="region"
|
|
27
34
|
:aria-label="label ?? text.label"
|
|
28
35
|
:aria-busy="loading ? 'true' : void 0"
|
|
29
36
|
:style="autoHeight || fill ? void 0 : { height }"
|
|
37
|
+
@keydown.capture="sheet.onKeyDown"
|
|
38
|
+
@pointerdown.capture="sheet.onPointerDown"
|
|
39
|
+
@mousedown.capture="sheet.onMouseDown"
|
|
40
|
+
@pointerover="sheet.onPointerOver"
|
|
30
41
|
>
|
|
31
42
|
<AgGridVue
|
|
32
43
|
v-if="mounted"
|
|
@@ -54,12 +65,15 @@
|
|
|
54
65
|
:header-height="headerHeight ?? gridOptions?.headerHeight"
|
|
55
66
|
:tooltip-show-delay="gridOptions?.tooltipShowDelay ?? 600"
|
|
56
67
|
:suppress-cell-focus="false"
|
|
68
|
+
:undo-redo-cell-editing="spreadsheet ? false : gridOptions?.undoRedoCellEditing"
|
|
57
69
|
v-on="gridEvents"
|
|
58
70
|
/>
|
|
59
71
|
<div v-else class="k-data-grid-placeholder" aria-hidden="true">
|
|
60
72
|
<Spinner decorative />
|
|
61
73
|
</div>
|
|
62
|
-
<p v-if="copied" class="k-data-grid-toast" aria-hidden="true">
|
|
74
|
+
<p v-if="notice || copied" class="k-data-grid-toast" aria-hidden="true">
|
|
75
|
+
{{ notice || text.copied }}
|
|
76
|
+
</p>
|
|
63
77
|
<LiveRegion :message="loading || !mounted ? text.loading : live.message.value" />
|
|
64
78
|
</div>
|
|
65
79
|
|
|
@@ -99,7 +113,7 @@
|
|
|
99
113
|
|
|
100
114
|
<script setup>
|
|
101
115
|
import { AgGridVue } from "ag-grid-vue3";
|
|
102
|
-
import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
|
|
116
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
|
|
103
117
|
import Pagination from "../components/data/Pagination.vue";
|
|
104
118
|
import Spinner from "../components/core/Spinner.vue";
|
|
105
119
|
import { useCopy } from "../composables/useCopy";
|
|
@@ -113,8 +127,10 @@ import { escapeHtml } from "../internal/core/text";
|
|
|
113
127
|
import {
|
|
114
128
|
registerGridEditorModules,
|
|
115
129
|
registerGridFilterModules,
|
|
116
|
-
registerGridModules
|
|
130
|
+
registerGridModules,
|
|
131
|
+
registerGridSpreadsheetModules
|
|
117
132
|
} from "./modules";
|
|
133
|
+
import { createGridSpreadsheet } from "./spreadsheet";
|
|
118
134
|
import { getGridTheme } from "./theme";
|
|
119
135
|
registerGridModules();
|
|
120
136
|
const props = defineProps({
|
|
@@ -150,25 +166,34 @@ const props = defineProps({
|
|
|
150
166
|
headerHeight: { type: Number, required: false },
|
|
151
167
|
headerText: { type: Function, required: false },
|
|
152
168
|
filterable: { type: Boolean, required: false },
|
|
153
|
-
editable: { type: Boolean, required: false }
|
|
169
|
+
editable: { type: Boolean, required: false },
|
|
170
|
+
spreadsheet: { type: Boolean, required: false }
|
|
154
171
|
});
|
|
155
|
-
const emit = defineEmits(["rowClick", "selectionChange", "sortChange", "filterChange", "copy"]);
|
|
172
|
+
const emit = defineEmits(["rowClick", "selectionChange", "sortChange", "filterChange", "copy", "cellsChange", "cellsInvalid", "dirtyChange", "rangeChange"]);
|
|
156
173
|
const query = defineModel("query", { type: String, ...{ default: "" } });
|
|
157
174
|
const page = defineModel("page", { type: Number, ...{ default: 1 } });
|
|
158
175
|
const pageSize = defineModel("pageSize", { type: Number, ...{ default: 25 } });
|
|
159
176
|
const sort = defineModel("sort", { type: null, ...{ default: void 0 } });
|
|
160
177
|
if (props.filterable) registerGridFilterModules();
|
|
161
178
|
function hasEditableColumn() {
|
|
162
|
-
return props.editable || Boolean(props.gridOptions?.defaultColDef?.editable) || props.columns.some((column) => Boolean(column.editable));
|
|
179
|
+
return props.editable || props.spreadsheet || Boolean(props.gridOptions?.defaultColDef?.editable) || props.columns.some((column) => Boolean(column.editable));
|
|
163
180
|
}
|
|
164
|
-
if (
|
|
181
|
+
if (props.spreadsheet) registerGridSpreadsheetModules();
|
|
182
|
+
else if (hasEditableColumn()) registerGridEditorModules();
|
|
165
183
|
const columnDefs = computed(() => {
|
|
166
184
|
const headerText = props.headerText;
|
|
167
|
-
|
|
185
|
+
const rules = props.spreadsheet ? sheet.classRules : void 0;
|
|
186
|
+
if (!headerText && !rules) return props.columns;
|
|
187
|
+
const defaultRules = props.gridOptions?.defaultColDef?.cellClassRules;
|
|
168
188
|
return props.columns.map((column) => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
let next = column;
|
|
190
|
+
if (headerText && column.headerName === void 0 && column.field) {
|
|
191
|
+
const headerName = headerText(String(column.field), column);
|
|
192
|
+
if (headerName !== void 0) next = { ...next, headerName };
|
|
193
|
+
}
|
|
194
|
+
if (rules)
|
|
195
|
+
next = { ...next, cellClassRules: { ...defaultRules, ...column.cellClassRules, ...rules } };
|
|
196
|
+
return next;
|
|
172
197
|
});
|
|
173
198
|
});
|
|
174
199
|
const { locale, t } = useLocale();
|
|
@@ -212,6 +237,52 @@ const selectedCount = ref(0);
|
|
|
212
237
|
const clientTotal = ref(0);
|
|
213
238
|
const clientPageCount = ref(1);
|
|
214
239
|
const { copied, copy } = useCopy();
|
|
240
|
+
const bodyElement = ref(null);
|
|
241
|
+
const notice = ref("");
|
|
242
|
+
let noticeTimer;
|
|
243
|
+
const noticeMessages = {
|
|
244
|
+
copied: "cellsCopied",
|
|
245
|
+
pasted: "cellsPasted",
|
|
246
|
+
filled: "cellsFilled",
|
|
247
|
+
cleared: "cellsCleared",
|
|
248
|
+
updated: "cellsUpdated",
|
|
249
|
+
undone: "changesUndone",
|
|
250
|
+
redone: "changesRedone"
|
|
251
|
+
};
|
|
252
|
+
function formatCount(template, count) {
|
|
253
|
+
return formatLocaleMessage(locale.value.code, template, { count });
|
|
254
|
+
}
|
|
255
|
+
function showNotice(kind, count, invalid) {
|
|
256
|
+
if (!count && !invalid) return;
|
|
257
|
+
const message = formatCount(text.value[noticeMessages[kind]], count);
|
|
258
|
+
notice.value = invalid ? `${message}, ${formatCount(text.value.invalidValues, invalid)}` : message;
|
|
259
|
+
live.announce(notice.value);
|
|
260
|
+
clearTimeout(noticeTimer);
|
|
261
|
+
noticeTimer = setTimeout(() => notice.value = "", 1600);
|
|
262
|
+
}
|
|
263
|
+
const sheet = createGridSpreadsheet({
|
|
264
|
+
enabled: () => Boolean(props.spreadsheet),
|
|
265
|
+
body: () => bodyElement.value,
|
|
266
|
+
gridOptions: () => props.gridOptions,
|
|
267
|
+
onChange: (event) => emit("cellsChange", event),
|
|
268
|
+
onInvalid: (event) => emit("cellsInvalid", event),
|
|
269
|
+
onDirty: (count) => emit("dirtyChange", count),
|
|
270
|
+
onRange: (range) => emit("rangeChange", range),
|
|
271
|
+
onCopy: (copiedText) => emit("copy", copiedText),
|
|
272
|
+
onSelect: (count) => live.announce(formatCount(text.value.cellsSelected, count)),
|
|
273
|
+
notify: showNotice
|
|
274
|
+
});
|
|
275
|
+
const { filling: sheetFilling, slotProps: sheetSlot } = sheet;
|
|
276
|
+
watch(
|
|
277
|
+
() => props.spreadsheet,
|
|
278
|
+
(enabled) => {
|
|
279
|
+
if (!enabled) sheet.disable();
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
onBeforeUnmount(() => {
|
|
283
|
+
clearTimeout(noticeTimer);
|
|
284
|
+
sheet.detach();
|
|
285
|
+
});
|
|
215
286
|
const paginationEnabled = computed(() => props.pagination || props.serverPagination);
|
|
216
287
|
const paginated = computed(() => Boolean(props.pagination) && !props.serverPagination);
|
|
217
288
|
const showFooter = computed(() => paginationEnabled.value || Boolean(props.selectable));
|
|
@@ -293,6 +364,7 @@ function persistState(event) {
|
|
|
293
364
|
}
|
|
294
365
|
function onGridReady(event) {
|
|
295
366
|
api.value = event.api;
|
|
367
|
+
sheet.attach(event.api);
|
|
296
368
|
const stored = readStoredState();
|
|
297
369
|
if (stored?.length) event.api.applyColumnState({ state: stored, applyOrder: true });
|
|
298
370
|
applySort();
|
|
@@ -369,6 +441,10 @@ function onFilterChanged(event) {
|
|
|
369
441
|
async function onCellContextMenu(event) {
|
|
370
442
|
if (!event.node || !event.column) return;
|
|
371
443
|
event.event?.preventDefault();
|
|
444
|
+
if (props.spreadsheet && sheet.containsCell(event.node.rowIndex, event.column.getColId())) {
|
|
445
|
+
await sheet.copyRange();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
372
448
|
const value = event.api.getCellValue({
|
|
373
449
|
rowNode: event.node,
|
|
374
450
|
colKey: event.column,
|
|
@@ -399,6 +475,7 @@ onMounted(() => {
|
|
|
399
475
|
mounted.value = true;
|
|
400
476
|
});
|
|
401
477
|
defineExpose({
|
|
478
|
+
...sheet.methods,
|
|
402
479
|
api,
|
|
403
480
|
getSelectedRows: () => api.value?.getSelectedRows() ?? [],
|
|
404
481
|
deselectAll: () => api.value?.deselectAll(),
|