@omnitend/dashboard-for-laravel 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/base/DButton.vue.d.ts +57 -2
- package/dist/components/extended/DXField.vue.d.ts +8 -4
- package/dist/dashboard-for-laravel.js +3190 -3106
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +6 -6
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +5 -1
- package/dist/types/index.d.ts +10 -0
- package/docs/public/api-reference.json +172 -4
- package/docs/public/docs-map.md +1 -1
- package/package.json +2 -1
- package/resources/css/theme.scss +17 -0
- package/resources/js/components/base/DButton.vue +170 -6
- package/resources/js/components/extended/DXField.vue +32 -11
- package/resources/js/components/extended/DXForm.vue +12 -8
- package/resources/js/components/extended/DXTable.vue +6 -7
- package/resources/js/types/index.ts +11 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -157,6 +157,16 @@ export interface FieldDefinition {
|
|
|
157
157
|
* layout. Falls back to the form-level `labelCols` when omitted.
|
|
158
158
|
*/
|
|
159
159
|
labelCols?: LabelCols;
|
|
160
|
+
/**
|
|
161
|
+
* Skip rendering the field's own label. In horizontal layout no label
|
|
162
|
+
* column is reserved either, so the control spans the full width. Useful
|
|
163
|
+
* for `switch` (and `checkbox`) fields, which render their own label
|
|
164
|
+
* inside the control — the outer row label would otherwise duplicate it,
|
|
165
|
+
* and the self-label then lines up with sibling row labels. Also used
|
|
166
|
+
* internally by `DXRepeater`'s `table` layout, where a column header
|
|
167
|
+
* already names the field.
|
|
168
|
+
*/
|
|
169
|
+
hideLabel?: boolean;
|
|
160
170
|
/**
|
|
161
171
|
* Component rendered for `type: "component"` fields. Receives
|
|
162
172
|
* `modelValue`, `field`, `model` props and emits `update:modelValue`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-08T06:35:48.024Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.17.0"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"name": "variant",
|
|
185
185
|
"type": "union",
|
|
186
186
|
"required": false,
|
|
187
|
-
"default": "
|
|
187
|
+
"default": "\"primary\"",
|
|
188
188
|
"description": "The visual style variant of the button"
|
|
189
189
|
},
|
|
190
190
|
{
|
|
@@ -192,10 +192,62 @@
|
|
|
192
192
|
"type": "Size",
|
|
193
193
|
"required": false,
|
|
194
194
|
"description": "The size of the button. Omit for the default (medium) size —\nbootstrap-vue-next 0.45 removed the no-op `'md'` value from `Size`."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"name": "disabled",
|
|
198
|
+
"type": "boolean",
|
|
199
|
+
"required": false,
|
|
200
|
+
"default": "false",
|
|
201
|
+
"description": "Disable the button. Combined with `loading` — either one disables it."
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"name": "loading",
|
|
205
|
+
"type": "boolean",
|
|
206
|
+
"required": false,
|
|
207
|
+
"default": "false",
|
|
208
|
+
"description": "Show an in-flight state: the button is disabled immediately and, after\n`spinnerDelay`, a trailing spinner appears (and the label swaps to\n`loadingText` if given)."
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "loadingText",
|
|
212
|
+
"type": "string",
|
|
213
|
+
"required": false,
|
|
214
|
+
"description": "Label shown in place of the default slot while the spinner is visible.\nOmit to keep the original label alongside the spinner."
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"name": "icon",
|
|
218
|
+
"type": "string",
|
|
219
|
+
"required": false,
|
|
220
|
+
"description": "Leading Bootstrap Icons glyph name, rendered as `<i class=\"bi bi-{icon}\">`.\ne.g. `icon=\"save\"`. Hidden while the loading spinner is visible."
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "block",
|
|
224
|
+
"type": "boolean",
|
|
225
|
+
"required": false,
|
|
226
|
+
"default": "false",
|
|
227
|
+
"description": "Full-width button (BS5 `w-100` idiom, replacing the dropped `.btn-block`)."
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "spinnerDelay",
|
|
231
|
+
"type": "number",
|
|
232
|
+
"required": false,
|
|
233
|
+
"default": "500",
|
|
234
|
+
"description": "Anti-flash: how long (ms) `loading` must stay true before the spinner is\nshown, so fast actions never flicker one in."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "minSpinnerTime",
|
|
238
|
+
"type": "number",
|
|
239
|
+
"required": false,
|
|
240
|
+
"default": "100",
|
|
241
|
+
"description": "Anti-flash: minimum time (ms) the spinner stays visible once shown, so a\nloading state that clears quickly doesn't strobe it away."
|
|
195
242
|
}
|
|
196
243
|
],
|
|
197
244
|
"events": [],
|
|
198
245
|
"slots": [
|
|
246
|
+
{
|
|
247
|
+
"name": "default",
|
|
248
|
+
"description": "",
|
|
249
|
+
"bindings": []
|
|
250
|
+
},
|
|
199
251
|
{
|
|
200
252
|
"name": "name",
|
|
201
253
|
"description": "",
|
|
@@ -1713,6 +1765,24 @@
|
|
|
1713
1765
|
"type": "string",
|
|
1714
1766
|
"required": false,
|
|
1715
1767
|
"description": "Error key for validation lookups (defaults to keyPath/field.key)."
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
"name": "layout",
|
|
1771
|
+
"type": "union",
|
|
1772
|
+
"required": false,
|
|
1773
|
+
"description": "Field layout: \"vertical\" (default, label above input) or \"horizontal\"\n(label left, input right). Normally set by DXForm (form-level, with\n`field.layout` taking precedence); pass directly when using DXField\nstandalone."
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
"name": "labelCols",
|
|
1777
|
+
"type": "LabelCols",
|
|
1778
|
+
"required": false,
|
|
1779
|
+
"description": "Label column width for horizontal layout (mirrors BFormGroup)."
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
"name": "hideLabel",
|
|
1783
|
+
"type": "boolean",
|
|
1784
|
+
"required": false,
|
|
1785
|
+
"description": "Skip rendering the field's own label entirely (only affects the\nstandard labelled-field branch). Used by DXRepeater's `table` layout,\nwhere a column header already names the field and a per-row label\nwould waste space in the cell."
|
|
1716
1786
|
}
|
|
1717
1787
|
],
|
|
1718
1788
|
"events": [],
|
|
@@ -1925,6 +1995,26 @@
|
|
|
1925
1995
|
"required": false,
|
|
1926
1996
|
"default": "true",
|
|
1927
1997
|
"description": "Auto-switch to the first tab containing a validation error."
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
"name": "card",
|
|
2001
|
+
"type": "boolean",
|
|
2002
|
+
"required": false,
|
|
2003
|
+
"default": "false",
|
|
2004
|
+
"description": "Wrap the form in a card for a visual boundary (mirrors DXTable's\n`card` prop). Tabbed forms render the tab nav as a BS5\ncard-header-tabs instead of double-wrapping. Off by default since\nDXForm is commonly embedded in a page card or modal already."
|
|
2005
|
+
},
|
|
2006
|
+
{
|
|
2007
|
+
"name": "layout",
|
|
2008
|
+
"type": "union",
|
|
2009
|
+
"required": false,
|
|
2010
|
+
"default": "\"vertical\"",
|
|
2011
|
+
"description": "Form-wide field layout: \"vertical\" (default, label above input) or\n\"horizontal\" (label left, input right). Overridable per-field via\n`field.layout`. A field with `span: true` always renders full-width,\nregardless of layout."
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"name": "labelCols",
|
|
2015
|
+
"type": "LabelCols",
|
|
2016
|
+
"required": false,
|
|
2017
|
+
"description": "Label column width for horizontal layout (mirrors BFormGroup's\n`labelCols`/`labelCols*` props). Overridable per-field via\n`field.labelCols`. Ignored when `layout` is \"vertical\"."
|
|
1928
2018
|
}
|
|
1929
2019
|
],
|
|
1930
2020
|
"events": [
|
|
@@ -1987,6 +2077,58 @@
|
|
|
1987
2077
|
}
|
|
1988
2078
|
]
|
|
1989
2079
|
},
|
|
2080
|
+
{
|
|
2081
|
+
"name": "`field(${field.key})`",
|
|
2082
|
+
"description": "Fully replaces a field's rendering (including its label), keyed by field key: `field(<key>)`. Unlike `value(<key>)`, bypasses DXField entirely — like `tab-content`, this also supersedes `field-before(<key>)`/`field-after(<key>)` for the same key.",
|
|
2083
|
+
"bindings": [
|
|
2084
|
+
{
|
|
2085
|
+
"name": "name",
|
|
2086
|
+
"title": "binding"
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
"title": "binding",
|
|
2090
|
+
"type": {
|
|
2091
|
+
"name": "FieldDefinition"
|
|
2092
|
+
},
|
|
2093
|
+
"name": "field",
|
|
2094
|
+
"description": "The field definition being replaced."
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
"title": "binding",
|
|
2098
|
+
"type": {
|
|
2099
|
+
"name": "object"
|
|
2100
|
+
},
|
|
2101
|
+
"name": "model",
|
|
2102
|
+
"description": "Live form data merged with `context`, for predicates."
|
|
2103
|
+
}
|
|
2104
|
+
]
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
"name": "`field-before(${field.key})`",
|
|
2108
|
+
"description": "Content inserted directly above a field, keyed by field key: `field-before(<key>)`. Not rendered when `field(<key>)` replaces the same field.",
|
|
2109
|
+
"bindings": [
|
|
2110
|
+
{
|
|
2111
|
+
"name": "name",
|
|
2112
|
+
"title": "binding"
|
|
2113
|
+
},
|
|
2114
|
+
{
|
|
2115
|
+
"title": "binding",
|
|
2116
|
+
"type": {
|
|
2117
|
+
"name": "FieldDefinition"
|
|
2118
|
+
},
|
|
2119
|
+
"name": "field",
|
|
2120
|
+
"description": "The field about to be rendered."
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
"title": "binding",
|
|
2124
|
+
"type": {
|
|
2125
|
+
"name": "object"
|
|
2126
|
+
},
|
|
2127
|
+
"name": "model",
|
|
2128
|
+
"description": "Live form data merged with `context`, for predicates."
|
|
2129
|
+
}
|
|
2130
|
+
]
|
|
2131
|
+
},
|
|
1990
2132
|
{
|
|
1991
2133
|
"name": "slotName",
|
|
1992
2134
|
"description": "Per-field overrides forwarded to DXField, keyed by field key: `value(<key>)`, `span(<key>)`, `info(<key>)`, `hint(<key>)`, `repeater-row(<key>)`.",
|
|
@@ -1997,6 +2139,32 @@
|
|
|
1997
2139
|
}
|
|
1998
2140
|
]
|
|
1999
2141
|
},
|
|
2142
|
+
{
|
|
2143
|
+
"name": "`field-after(${field.key})`",
|
|
2144
|
+
"description": "Content inserted directly below a field, keyed by field key: `field-after(<key>)`. Not rendered when `field(<key>)` replaces the same field.",
|
|
2145
|
+
"bindings": [
|
|
2146
|
+
{
|
|
2147
|
+
"name": "name",
|
|
2148
|
+
"title": "binding"
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
"title": "binding",
|
|
2152
|
+
"type": {
|
|
2153
|
+
"name": "FieldDefinition"
|
|
2154
|
+
},
|
|
2155
|
+
"name": "field",
|
|
2156
|
+
"description": "The field that was just rendered."
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
"title": "binding",
|
|
2160
|
+
"type": {
|
|
2161
|
+
"name": "object"
|
|
2162
|
+
},
|
|
2163
|
+
"name": "model",
|
|
2164
|
+
"description": "Live form data merged with `context`, for predicates."
|
|
2165
|
+
}
|
|
2166
|
+
]
|
|
2167
|
+
},
|
|
2000
2168
|
{
|
|
2001
2169
|
"name": "`tab-after(${tab.key})`",
|
|
2002
2170
|
"description": "Content inserted below a tab's fields, keyed by tab (slot name `tab-after(<tabKey>)`).",
|
|
@@ -2130,7 +2298,7 @@
|
|
|
2130
2298
|
"slots": [
|
|
2131
2299
|
{
|
|
2132
2300
|
"name": "row",
|
|
2133
|
-
"description": "Custom layout for a single repeater row, replacing the default sub-field stack.",
|
|
2301
|
+
"description": "Custom layout for a single repeater row, replacing the default sub-field stack (including the built-in Remove control — call `remove()` yourself). In table layout, rendered as the `<tr>`'s children: supply one `<td>` per sub-field plus a trailing `<td>` to match the header's column count (the header always includes an empty column for the built-in remove button).",
|
|
2134
2302
|
"bindings": [
|
|
2135
2303
|
{
|
|
2136
2304
|
"title": "binding",
|
package/docs/public/docs-map.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Documentation Map
|
|
2
2
|
|
|
3
3
|
> Auto-generated hierarchical overview of all documentation
|
|
4
|
-
> Last updated: 2026-07-
|
|
4
|
+
> Last updated: 2026-07-08T06:35:48.080Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnitend/dashboard-for-laravel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/dashboard-for-laravel.umd.cjs",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"@vue/test-utils": "^2.4.6",
|
|
89
89
|
"astro": "^5.15.1",
|
|
90
90
|
"bootstrap": "^5.3.8",
|
|
91
|
+
"bootstrap-icons": "^1.13.1",
|
|
91
92
|
"chart.js": "^4.5.1",
|
|
92
93
|
"happy-dom": "^20.0.8",
|
|
93
94
|
"pagefind": "^1.4.0",
|
package/resources/css/theme.scss
CHANGED
|
@@ -152,6 +152,23 @@ $dashboard-navbar-height: 64px;
|
|
|
152
152
|
|
|
153
153
|
@import "bootstrap-vue-next/dist/bootstrap-vue-next.css";
|
|
154
154
|
|
|
155
|
+
// ----------------------------------------------------------------------------
|
|
156
|
+
// Import Bootstrap Icons
|
|
157
|
+
// ----------------------------------------------------------------------------
|
|
158
|
+
// Ships the icon font so `DButton`'s `icon` prop (and any consumer `<i class="bi
|
|
159
|
+
// bi-*">`) renders without the consumer wiring up their own icon set.
|
|
160
|
+
//
|
|
161
|
+
// Imported as SCSS (not the prebuilt CSS) so we can override the font `src` to
|
|
162
|
+
// woff2 ONLY — dropping bootstrap-icons' legacy `woff` fallback. In the library
|
|
163
|
+
// build Vite inlines CSS `url()` assets as data URIs, so shipping both formats
|
|
164
|
+
// doubled the embedded font bytes in `dist/style.css` for no benefit (every
|
|
165
|
+
// browser we target reads woff2). The override must come BEFORE the import so
|
|
166
|
+
// bootstrap-icons' own `!default` src doesn't re-add the woff. The bare
|
|
167
|
+
// `bootstrap-icons/...` specifier resolves through Vite's module resolver.
|
|
168
|
+
$bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2")
|
|
169
|
+
format("woff2");
|
|
170
|
+
@import "bootstrap-icons/font/bootstrap-icons";
|
|
171
|
+
|
|
155
172
|
// ----------------------------------------------------------------------------
|
|
156
173
|
// Forms
|
|
157
174
|
// ----------------------------------------------------------------------------
|
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* DButton - A type-safe wrapper around Bootstrap Vue Next's BButton component
|
|
4
4
|
*
|
|
5
|
+
* Adds three base-level button behaviours on top of `variant`/`size`:
|
|
6
|
+
* - `loading` — disables the button and shows a trailing spinner while an async
|
|
7
|
+
* action is in flight, with anti-flash timing (no spinner for sub-`spinnerDelay`
|
|
8
|
+
* actions; a `minSpinnerTime` floor once shown so it never strobes).
|
|
9
|
+
* - `icon` — a leading Bootstrap Icons glyph (e.g. `icon="save"` → `bi bi-save`).
|
|
10
|
+
* - `block` — full-width button (the BS5 replacement for the dropped `.btn-block`).
|
|
11
|
+
*
|
|
5
12
|
* @component
|
|
6
13
|
* @example
|
|
7
14
|
* ```vue
|
|
8
15
|
* <DButton variant="primary" size="lg">Click Me</DButton>
|
|
9
|
-
* <DButton
|
|
16
|
+
* <DButton icon="save" variant="primary">Save</DButton>
|
|
17
|
+
* <DButton :loading="isSaving" loading-text="Saving…">Save</DButton>
|
|
18
|
+
* <DButton block>Continue</DButton>
|
|
10
19
|
* ```
|
|
11
20
|
*/
|
|
12
21
|
import { BButton } from "bootstrap-vue-next";
|
|
13
22
|
import type { ButtonVariant, Size } from "bootstrap-vue-next";
|
|
14
|
-
import { useSlots } from "vue";
|
|
23
|
+
import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue";
|
|
24
|
+
import DSpinner from "./DSpinner.vue";
|
|
15
25
|
|
|
16
26
|
const slots = useSlots();
|
|
17
27
|
|
|
@@ -37,21 +47,175 @@ interface Props {
|
|
|
37
47
|
* bootstrap-vue-next 0.45 removed the no-op `'md'` value from `Size`.
|
|
38
48
|
*/
|
|
39
49
|
size?: Size;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Disable the button. Combined with `loading` — either one disables it.
|
|
53
|
+
*/
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Show an in-flight state: the button is disabled immediately and, after
|
|
58
|
+
* `spinnerDelay`, a trailing spinner appears (and the label swaps to
|
|
59
|
+
* `loadingText` if given).
|
|
60
|
+
*
|
|
61
|
+
* Diverges from bvn: `BButton` has a native `loading`/`loadingText` that
|
|
62
|
+
* shows its spinner immediately. We intercept both and render our own
|
|
63
|
+
* anti-flash spinner instead, so bvn's native loading never fires. See
|
|
64
|
+
* DIVERGENCES.md.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
loading?: boolean;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Label shown in place of the default slot while the spinner is visible.
|
|
71
|
+
* Omit to keep the original label alongside the spinner. (See `loading` re:
|
|
72
|
+
* the bvn divergence.)
|
|
73
|
+
*/
|
|
74
|
+
loadingText?: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Leading Bootstrap Icons glyph name, rendered as `<i class="bi bi-{icon}">`.
|
|
78
|
+
* e.g. `icon="save"`. Hidden while the loading spinner is visible.
|
|
79
|
+
*
|
|
80
|
+
* Diverges from bvn: `BButton`'s native `icon` is a `boolean` (icon-button
|
|
81
|
+
* styling mode). We redefine it as a glyph-name `string`, so bvn's boolean
|
|
82
|
+
* icon mode is unreachable through `DButton`. See DIVERGENCES.md.
|
|
83
|
+
*/
|
|
84
|
+
icon?: string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Full-width button (BS5 `w-100` idiom, replacing the dropped `.btn-block`).
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
block?: boolean;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Anti-flash: how long (ms) `loading` must stay true before the spinner is
|
|
94
|
+
* shown, so fast actions never flicker one in.
|
|
95
|
+
* @default 500
|
|
96
|
+
*/
|
|
97
|
+
spinnerDelay?: number;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Anti-flash: minimum time (ms) the spinner stays visible once shown, so a
|
|
101
|
+
* loading state that clears quickly doesn't strobe it away.
|
|
102
|
+
* @default 100
|
|
103
|
+
*/
|
|
104
|
+
minSpinnerTime?: number;
|
|
40
105
|
}
|
|
41
106
|
|
|
42
|
-
withDefaults(defineProps<Props>(), {
|
|
43
|
-
variant:
|
|
107
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
108
|
+
variant: "primary",
|
|
109
|
+
disabled: false,
|
|
110
|
+
loading: false,
|
|
111
|
+
block: false,
|
|
112
|
+
spinnerDelay: 500,
|
|
113
|
+
minSpinnerTime: 100,
|
|
44
114
|
});
|
|
115
|
+
|
|
116
|
+
// --- Spinner anti-flash state machine -------------------------------------
|
|
117
|
+
// `loading` disables the button instantly (prevents double-submit), but the
|
|
118
|
+
// *spinner visual* is gated: it only appears once `loading` has held for
|
|
119
|
+
// `spinnerDelay`, and once shown it stays for at least `minSpinnerTime`.
|
|
120
|
+
const showSpinner = ref(false);
|
|
121
|
+
let delayTimer: ReturnType<typeof setTimeout> | null = null;
|
|
122
|
+
let minTimer: ReturnType<typeof setTimeout> | null = null;
|
|
123
|
+
let shownAt = 0;
|
|
124
|
+
|
|
125
|
+
function clearTimer(timer: ReturnType<typeof setTimeout> | null) {
|
|
126
|
+
if (timer !== null) clearTimeout(timer);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
watch(
|
|
130
|
+
() => props.loading,
|
|
131
|
+
(isLoading) => {
|
|
132
|
+
// The spinner is a client-only concern. On the server there's no unmount
|
|
133
|
+
// hook to clear a scheduled timer, and the spinner never paints before
|
|
134
|
+
// hydration anyway (it's gated behind `spinnerDelay`), so skip the timers
|
|
135
|
+
// entirely during SSR — `showSpinner` stays false, matching the initial
|
|
136
|
+
// client render and avoiding a dangling server-side timeout.
|
|
137
|
+
if (typeof window === "undefined") return;
|
|
138
|
+
if (isLoading) {
|
|
139
|
+
// Cancel any pending hide — we're loading again, keep the spinner up.
|
|
140
|
+
clearTimer(minTimer);
|
|
141
|
+
minTimer = null;
|
|
142
|
+
// Already showing, or already counting down to show: nothing to do.
|
|
143
|
+
if (showSpinner.value || delayTimer !== null) return;
|
|
144
|
+
delayTimer = setTimeout(() => {
|
|
145
|
+
delayTimer = null;
|
|
146
|
+
showSpinner.value = true;
|
|
147
|
+
shownAt = Date.now();
|
|
148
|
+
}, props.spinnerDelay);
|
|
149
|
+
} else {
|
|
150
|
+
// Stopped before the spinner ever appeared → cancel, no flash at all.
|
|
151
|
+
if (delayTimer !== null) {
|
|
152
|
+
clearTimeout(delayTimer);
|
|
153
|
+
delayTimer = null;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!showSpinner.value) return;
|
|
157
|
+
const remaining = props.minSpinnerTime - (Date.now() - shownAt);
|
|
158
|
+
if (remaining <= 0) {
|
|
159
|
+
showSpinner.value = false;
|
|
160
|
+
} else if (minTimer === null) {
|
|
161
|
+
minTimer = setTimeout(() => {
|
|
162
|
+
minTimer = null;
|
|
163
|
+
showSpinner.value = false;
|
|
164
|
+
}, remaining);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{ immediate: true },
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
onBeforeUnmount(() => {
|
|
172
|
+
clearTimer(delayTimer);
|
|
173
|
+
clearTimer(minTimer);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// BButton owns the default slot (we compose icon/spinner/label into it); any
|
|
177
|
+
// other named slots are forwarded through untouched.
|
|
178
|
+
const passthroughSlotNames = computed(() =>
|
|
179
|
+
Object.keys(slots).filter((name) => name !== "default"),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Bootstrap utility classes are global, so they apply regardless of whether the
|
|
183
|
+
// wrapper's scope-id reaches BButton's rendered <button> — avoiding the scoped
|
|
184
|
+
// `:deep()`-on-a-B-root trap. Only add the flex row when there's an adornment
|
|
185
|
+
// to space out.
|
|
186
|
+
const hasAdornment = computed(
|
|
187
|
+
() => Boolean(props.icon) || showSpinner.value,
|
|
188
|
+
);
|
|
45
189
|
</script>
|
|
46
190
|
|
|
47
191
|
<template>
|
|
48
192
|
<BButton
|
|
49
193
|
:variant="variant"
|
|
50
194
|
:size="size"
|
|
195
|
+
:disabled="disabled || loading || showSpinner"
|
|
196
|
+
:aria-busy="loading || showSpinner || undefined"
|
|
197
|
+
:class="[
|
|
198
|
+
{ 'd-inline-flex align-items-center justify-content-center gap-2': hasAdornment },
|
|
199
|
+
{ 'w-100': block },
|
|
200
|
+
]"
|
|
51
201
|
v-bind="$attrs"
|
|
52
202
|
>
|
|
53
|
-
<
|
|
54
|
-
|
|
203
|
+
<i
|
|
204
|
+
v-if="icon && !showSpinner"
|
|
205
|
+
:class="['bi', `bi-${icon}`]"
|
|
206
|
+
aria-hidden="true"
|
|
207
|
+
></i>
|
|
208
|
+
<template v-if="showSpinner && loadingText">{{ loadingText }}</template>
|
|
209
|
+
<slot v-else />
|
|
210
|
+
<DSpinner v-if="showSpinner" small />
|
|
211
|
+
|
|
212
|
+
<!-- Forward any other named slots untouched, with their slot props. -->
|
|
213
|
+
<template
|
|
214
|
+
v-for="name in passthroughSlotNames"
|
|
215
|
+
:key="name"
|
|
216
|
+
#[name]="slotProps"
|
|
217
|
+
>
|
|
218
|
+
<slot :name="name" v-bind="slotProps" />
|
|
55
219
|
</template>
|
|
56
220
|
</BButton>
|
|
57
221
|
</template>
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
:class="field.class || 'mb-3'"
|
|
36
36
|
v-bind="horizontalAttrs"
|
|
37
37
|
>
|
|
38
|
-
|
|
38
|
+
<!-- The inner label (beside the checkbox) always names the control, so
|
|
39
|
+
hideLabel drops only the redundant outer row label; horizontalAttrs
|
|
40
|
+
then reserves no label column, letting the control span full width. -->
|
|
41
|
+
<template v-if="!hideLabel" #label>
|
|
39
42
|
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
40
43
|
</template>
|
|
41
44
|
<slot
|
|
@@ -119,7 +122,10 @@
|
|
|
119
122
|
:class="[field.class || 'mb-3', 'dx-switch', { 'dx-switch--on': switchIsOn }]"
|
|
120
123
|
v-bind="horizontalAttrs"
|
|
121
124
|
>
|
|
122
|
-
|
|
125
|
+
<!-- The switch renders its own label inside the control, so hideLabel
|
|
126
|
+
drops only the redundant outer row label; horizontalAttrs then
|
|
127
|
+
reserves no label column, letting the toggle span full width. -->
|
|
128
|
+
<template v-if="!hideLabel" #label>
|
|
123
129
|
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
124
130
|
</template>
|
|
125
131
|
<slot
|
|
@@ -203,7 +209,7 @@
|
|
|
203
209
|
<!-- Repeater: nested, repeatable sub-form -->
|
|
204
210
|
<div v-else-if="field.type === 'repeater'" :class="field.class || 'mb-3'">
|
|
205
211
|
<DFormGroup v-bind="repeaterHorizontalAttrs">
|
|
206
|
-
<template #label>
|
|
212
|
+
<template v-if="!hideLabel" #label>
|
|
207
213
|
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
208
214
|
</template>
|
|
209
215
|
<DXRepeater
|
|
@@ -503,10 +509,14 @@ interface Props {
|
|
|
503
509
|
labelCols?: LabelCols;
|
|
504
510
|
|
|
505
511
|
/**
|
|
506
|
-
* Skip rendering the field's own label
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
512
|
+
* Skip rendering the field's own (outer) label. Applies to the standard
|
|
513
|
+
* labelled-field branch and to the checkbox/switch/repeater branches. In
|
|
514
|
+
* horizontal layout no label column is reserved either, so the control
|
|
515
|
+
* spans the full width (a switch/checkbox self-label then lines up with
|
|
516
|
+
* sibling row labels). Used by DXRepeater's `table` layout (a column
|
|
517
|
+
* header already names the field) and by consumers hiding the redundant
|
|
518
|
+
* row label on a switch/checkbox, which render their own label inside the
|
|
519
|
+
* control (#78).
|
|
510
520
|
*/
|
|
511
521
|
hideLabel?: boolean;
|
|
512
522
|
}
|
|
@@ -545,9 +555,17 @@ function labelColsAttrs(cols: LabelCols | undefined): Record<string, any> {
|
|
|
545
555
|
}
|
|
546
556
|
|
|
547
557
|
// Default to a 3-column label when horizontal but no width was configured,
|
|
548
|
-
// so `layout: "horizontal"` alone is enough to see the effect.
|
|
558
|
+
// so `layout: "horizontal"` alone is enough to see the effect. When the
|
|
559
|
+
// field's label is hidden, reserve no label column at all — BFormGroup would
|
|
560
|
+
// otherwise still emit an empty label column (it goes horizontal whenever any
|
|
561
|
+
// labelCols are set, regardless of whether a label exists), leaving the
|
|
562
|
+
// control indented in the input column beside dead space. Dropping the column
|
|
563
|
+
// lets the control span full width, so a switch/checkbox self-label lines up
|
|
564
|
+
// with sibling row labels (#78).
|
|
549
565
|
const horizontalAttrs = computed<Record<string, any>>(() =>
|
|
550
|
-
isHorizontal.value
|
|
566
|
+
isHorizontal.value && !props.hideLabel
|
|
567
|
+
? labelColsAttrs(props.labelCols ?? 3)
|
|
568
|
+
: {},
|
|
551
569
|
);
|
|
552
570
|
|
|
553
571
|
// A table-layout repeater is inherently wide — squeezing its own label into a
|
|
@@ -846,9 +864,12 @@ onBeforeUnmount(() => {
|
|
|
846
864
|
`justify-content: space-between` on a full-width box used to leave a wide
|
|
847
865
|
unclickable gap between the label and the toggle (only the tiny toggle
|
|
848
866
|
itself was a real click target); shrinking the box to fit its content
|
|
849
|
-
removes that dead zone entirely.
|
|
867
|
+
removes that dead zone entirely. Uses block-level `flex` (not `inline-flex`)
|
|
868
|
+
so the box stays shrink-to-fit yet still takes its own line: an inline-level
|
|
869
|
+
box let a following `<small class="form-text">` hint flow inline beside it
|
|
870
|
+
instead of dropping below the control like every other field type (#79). */
|
|
850
871
|
.dx-switch :deep(.form-check) {
|
|
851
|
-
display:
|
|
872
|
+
display: flex;
|
|
852
873
|
align-items: center;
|
|
853
874
|
gap: 0.75rem;
|
|
854
875
|
width: fit-content;
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
:model="model"
|
|
89
89
|
:layout="field.layout ?? layout"
|
|
90
90
|
:label-cols="field.labelCols ?? labelCols"
|
|
91
|
+
:hide-label="field.hideLabel"
|
|
91
92
|
>
|
|
92
93
|
<template
|
|
93
94
|
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
@@ -151,6 +152,7 @@
|
|
|
151
152
|
:model="model"
|
|
152
153
|
:layout="field.layout ?? layout"
|
|
153
154
|
:label-cols="field.labelCols ?? labelCols"
|
|
155
|
+
:hide-label="field.hideLabel"
|
|
154
156
|
>
|
|
155
157
|
<template
|
|
156
158
|
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
@@ -176,11 +178,12 @@
|
|
|
176
178
|
v-if="showSubmit"
|
|
177
179
|
type="submit"
|
|
178
180
|
variant="primary"
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
block
|
|
182
|
+
:loading="resolvedForm.processing"
|
|
183
|
+
:loading-text="submitLoadingText"
|
|
184
|
+
class="mt-3"
|
|
181
185
|
>
|
|
182
|
-
|
|
183
|
-
<span v-else>{{ submitText }}</span>
|
|
186
|
+
{{ submitText }}
|
|
184
187
|
</DButton>
|
|
185
188
|
|
|
186
189
|
<!--
|
|
@@ -202,11 +205,12 @@
|
|
|
202
205
|
v-if="showSubmit"
|
|
203
206
|
type="submit"
|
|
204
207
|
variant="primary"
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
block
|
|
209
|
+
:loading="resolvedForm.processing"
|
|
210
|
+
:loading-text="submitLoadingText"
|
|
211
|
+
class="mt-3"
|
|
207
212
|
>
|
|
208
|
-
|
|
209
|
-
<span v-else>{{ submitText }}</span>
|
|
213
|
+
{{ submitText }}
|
|
210
214
|
</DButton>
|
|
211
215
|
|
|
212
216
|
<!--
|