nuxt-ui-tools 1.21.0 → 1.22.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/dist/module.json +1 -1
- package/dist/runtime/form/components/utils/form-option-leading.d.vue.ts +3 -0
- package/dist/runtime/form/components/utils/form-option-leading.vue +3 -2
- package/dist/runtime/form/components/utils/form-option-leading.vue.d.ts +3 -0
- package/dist/runtime/form/components/utils/form-option-token.d.vue.ts +15 -0
- package/dist/runtime/form/components/utils/form-option-token.vue +29 -0
- package/dist/runtime/form/components/utils/form-option-token.vue.d.ts +15 -0
- package/dist/runtime/form/components/utils/form-option-tokens.d.vue.ts +21 -0
- package/dist/runtime/form/components/utils/form-option-tokens.vue +26 -0
- package/dist/runtime/form/components/utils/form-option-tokens.vue.d.ts +21 -0
- package/dist/runtime/form/fields/auto-complete/component.vue +14 -1
- package/dist/runtime/form/fields/hierarchy/component.vue +72 -3
- package/dist/runtime/form/fields/select/component.vue +16 -1
- package/dist/runtime/form/fields/switch-group/component.vue +7 -2
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -13,11 +13,14 @@ type __VLS_Props = {
|
|
|
13
13
|
avatarSize?: string;
|
|
14
14
|
/** Classes of the box the chip dot is centered in. */
|
|
15
15
|
chipClass?: string;
|
|
16
|
+
/** Nuxt UI chip size: `sm` for the small text of a token. */
|
|
17
|
+
chipSize?: 'sm' | 'md';
|
|
16
18
|
};
|
|
17
19
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
18
20
|
iconClass: string;
|
|
19
21
|
avatarSize: string;
|
|
20
22
|
chipClass: string;
|
|
23
|
+
chipSize: "sm" | "md";
|
|
21
24
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
25
|
declare const _default: typeof __VLS_export;
|
|
23
26
|
export default _default;
|
|
@@ -8,7 +8,8 @@ const props = defineProps({
|
|
|
8
8
|
option: { type: Object, required: false },
|
|
9
9
|
iconClass: { type: String, required: false, default: "size-4 text-muted" },
|
|
10
10
|
avatarSize: { type: String, required: false, default: "xs" },
|
|
11
|
-
chipClass: { type: String, required: false, default: "size-4" }
|
|
11
|
+
chipClass: { type: String, required: false, default: "size-4" },
|
|
12
|
+
chipSize: { type: String, required: false, default: "md" }
|
|
12
13
|
});
|
|
13
14
|
const size = computed(() => AVATAR_SIZES.find((entry) => entry === props.avatarSize) ?? "xs");
|
|
14
15
|
</script>
|
|
@@ -36,6 +37,6 @@ const size = computed(() => AVATAR_SIZES.find((entry) => entry === props.avatarS
|
|
|
36
37
|
aria-hidden="true"
|
|
37
38
|
data-form-option-leading="chip"
|
|
38
39
|
>
|
|
39
|
-
<UChip standalone inset size="
|
|
40
|
+
<UChip standalone inset :size="chipSize" :color="option.chip.color" />
|
|
40
41
|
</span>
|
|
41
42
|
</template>
|
|
@@ -13,11 +13,14 @@ type __VLS_Props = {
|
|
|
13
13
|
avatarSize?: string;
|
|
14
14
|
/** Classes of the box the chip dot is centered in. */
|
|
15
15
|
chipClass?: string;
|
|
16
|
+
/** Nuxt UI chip size: `sm` for the small text of a token. */
|
|
17
|
+
chipSize?: 'sm' | 'md';
|
|
16
18
|
};
|
|
17
19
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
18
20
|
iconClass: string;
|
|
19
21
|
avatarSize: string;
|
|
20
22
|
chipClass: string;
|
|
23
|
+
chipSize: "sm" | "md";
|
|
21
24
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
25
|
declare const _default: typeof __VLS_export;
|
|
23
26
|
export default _default;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* One chosen option inside a token or a tag: its icon, avatar or chip, then its label, both sized
|
|
5
|
+
* for the small text of a token. A `renderValue` function draws the content instead.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Props = {
|
|
8
|
+
option: ResolvedFormOption;
|
|
9
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
10
|
+
/** Shown instead of the label, such as the path of a hierarchy option. */
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
|
+
declare const _default: typeof __VLS_export;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import FormOptionLeading from "./form-option-leading.vue";
|
|
3
|
+
import FormOptionRender from "./form-option-render";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
option: { type: Object, required: true },
|
|
6
|
+
render: { type: Function, required: false },
|
|
7
|
+
label: { type: String, required: false }
|
|
8
|
+
});
|
|
9
|
+
function content() {
|
|
10
|
+
const { item } = props.option;
|
|
11
|
+
return item === void 0 ? null : props.render?.({ item, option: props.option });
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<span class="inline-flex max-w-full min-w-0 items-center gap-1" data-form-option-token-content>
|
|
17
|
+
<FormOptionRender v-if="render && option.item !== void 0" :content="content" />
|
|
18
|
+
<template v-else>
|
|
19
|
+
<FormOptionLeading
|
|
20
|
+
:option="option"
|
|
21
|
+
icon-class="size-3.5 text-muted"
|
|
22
|
+
avatar-size="3xs"
|
|
23
|
+
chip-class="size-3"
|
|
24
|
+
chip-size="sm"
|
|
25
|
+
/>
|
|
26
|
+
<span class="min-w-0 truncate">{{ label ?? option.label }}</span>
|
|
27
|
+
</template>
|
|
28
|
+
</span>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* One chosen option inside a token or a tag: its icon, avatar or chip, then its label, both sized
|
|
5
|
+
* for the small text of a token. A `renderValue` function draws the content instead.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Props = {
|
|
8
|
+
option: ResolvedFormOption;
|
|
9
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
10
|
+
/** Shown instead of the label, such as the path of a hierarchy option. */
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
|
+
declare const _default: typeof __VLS_export;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The chosen options of a multiple selection as its closed control shows them: one token each,
|
|
5
|
+
* with the option's icon, avatar or chip before its label, wrapping onto more lines when they do
|
|
6
|
+
* not fit. A token is no taller than a line of text, so one row keeps the control as high as the
|
|
7
|
+
* inputs beside it. The tokens ignore the pointer, so a click still opens the control. They look
|
|
8
|
+
* like the tags of a multiple auto-complete, so both read alike. A visually hidden comma between
|
|
9
|
+
* them keeps the value reading as a list ("Euro, Dollar") for assistive technology.
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
options: readonly ResolvedFormOption[];
|
|
13
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
14
|
+
/** Shown instead of each label, such as the path of a hierarchy option. */
|
|
15
|
+
labelOf?: (option: ResolvedFormOption) => string;
|
|
16
|
+
/** Classes of each token. */
|
|
17
|
+
tokenClass?: string;
|
|
18
|
+
};
|
|
19
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { formOptionKey } from "../../utils/options";
|
|
3
|
+
import { mergeFormUiClass } from "../../utils/ui";
|
|
4
|
+
import FormOptionToken from "./form-option-token.vue";
|
|
5
|
+
defineProps({
|
|
6
|
+
options: { type: Array, required: true },
|
|
7
|
+
render: { type: Function, required: false },
|
|
8
|
+
labelOf: { type: Function, required: false },
|
|
9
|
+
tokenClass: { type: String, required: false }
|
|
10
|
+
});
|
|
11
|
+
const TOKEN = "inline-flex max-w-full min-w-0 items-center rounded-sm bg-elevated px-1.5 py-0.5 text-xs font-medium text-default ring ring-accented ring-inset";
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<span
|
|
16
|
+
class="pointer-events-none flex min-w-0 flex-1 flex-wrap gap-1 text-start"
|
|
17
|
+
data-form-option-tokens
|
|
18
|
+
>
|
|
19
|
+
<template v-for="(option, index) in options" :key="formOptionKey(option.value)">
|
|
20
|
+
<span :class="mergeFormUiClass(TOKEN, tokenClass)" data-form-option-token>
|
|
21
|
+
<FormOptionToken :option="option" :render="render" :label="labelOf?.(option)" />
|
|
22
|
+
</span>
|
|
23
|
+
<span v-if="index < options.length - 1" class="sr-only">, </span>
|
|
24
|
+
</template>
|
|
25
|
+
</span>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The chosen options of a multiple selection as its closed control shows them: one token each,
|
|
5
|
+
* with the option's icon, avatar or chip before its label, wrapping onto more lines when they do
|
|
6
|
+
* not fit. A token is no taller than a line of text, so one row keeps the control as high as the
|
|
7
|
+
* inputs beside it. The tokens ignore the pointer, so a click still opens the control. They look
|
|
8
|
+
* like the tags of a multiple auto-complete, so both read alike. A visually hidden comma between
|
|
9
|
+
* them keeps the value reading as a list ("Euro, Dollar") for assistive technology.
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
options: readonly ResolvedFormOption[];
|
|
13
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
14
|
+
/** Shown instead of each label, such as the path of a hierarchy option. */
|
|
15
|
+
labelOf?: (option: ResolvedFormOption) => string;
|
|
16
|
+
/** Classes of each token. */
|
|
17
|
+
tokenClass?: string;
|
|
18
|
+
};
|
|
19
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -7,10 +7,11 @@ import FormOptionItemLabel from "../../components/utils/form-option-item-label.v
|
|
|
7
7
|
import FormOptionLeading from "../../components/utils/form-option-leading.vue";
|
|
8
8
|
import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
|
|
9
9
|
import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
|
|
10
|
+
import FormOptionToken from "../../components/utils/form-option-token.vue";
|
|
10
11
|
import FormOptionValueOverlay from "../../components/utils/form-option-value-overlay.vue";
|
|
11
12
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
12
13
|
import { useOptionMenu } from "../../composables/use-option-menu";
|
|
13
|
-
import { leadingVisualPadding } from "../../utils/options";
|
|
14
|
+
import { formOptionKey, leadingVisualPadding } from "../../utils/options";
|
|
14
15
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
15
16
|
import { mergeFormUiClass } from "../../utils/ui";
|
|
16
17
|
const props = defineProps({
|
|
@@ -102,6 +103,14 @@ async function handleNativeCreate(label) {
|
|
|
102
103
|
}
|
|
103
104
|
await handleCreate(normalizedLabel);
|
|
104
105
|
}
|
|
106
|
+
function tagOption(value) {
|
|
107
|
+
const optionValue = typeof value === "number" || typeof value === "boolean" ? value : String(value);
|
|
108
|
+
const key = formOptionKey(optionValue);
|
|
109
|
+
return options.selectedItems.value.find((option) => formOptionKey(option.value) === key) ?? {
|
|
110
|
+
label: String(value),
|
|
111
|
+
value: optionValue
|
|
112
|
+
};
|
|
113
|
+
}
|
|
105
114
|
function isOptionValue(value) {
|
|
106
115
|
return isString(value) || isNumber(value) || isBoolean(value);
|
|
107
116
|
}
|
|
@@ -151,6 +160,10 @@ function isOptionValue(value) {
|
|
|
151
160
|
/>
|
|
152
161
|
</template>
|
|
153
162
|
|
|
163
|
+
<template v-if="fieldProps.multiple" #tags-item-text="{ item }">
|
|
164
|
+
<FormOptionToken :option="tagOption(item)" :render="menu.renderValue.value" />
|
|
165
|
+
</template>
|
|
166
|
+
|
|
154
167
|
<template #item-leading="{ item }">
|
|
155
168
|
<FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
|
|
156
169
|
</template>
|
|
@@ -10,10 +10,12 @@ import { computed, ref, watch } from "vue";
|
|
|
10
10
|
import { useUiToolsLocale } from "../../../i18n/use-locale";
|
|
11
11
|
import FormCompositeControl from "../../components/renderer/form-composite-control.vue";
|
|
12
12
|
import FormFieldShell from "../../components/renderer/form-field-shell.vue";
|
|
13
|
+
import FormOptionLeading from "../../components/utils/form-option-leading.vue";
|
|
13
14
|
import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
|
|
15
|
+
import FormOptionTokens from "../../components/utils/form-option-tokens.vue";
|
|
14
16
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
15
17
|
import { useFormUi } from "../../composables/use-form-ui";
|
|
16
|
-
import { formOptionKey } from "../../utils/options";
|
|
18
|
+
import { formOptionKey, leadingVisualPadding, resolveOptionLeading } from "../../utils/options";
|
|
17
19
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
18
20
|
import { resolveFormText } from "../../utils/text";
|
|
19
21
|
import { mergeFormUiClass } from "../../utils/ui";
|
|
@@ -147,8 +149,43 @@ const treeSelectLabel = computed(() => {
|
|
|
147
149
|
return props.field.type === "tree-select" && fieldProps.value.showPath ? selected.pathLabel : String(selected.label);
|
|
148
150
|
});
|
|
149
151
|
const fieldLabel = computed(() => resolveFormText(props.field.label));
|
|
152
|
+
const optionEntries = computed(() => {
|
|
153
|
+
const entries = /* @__PURE__ */ new Map();
|
|
154
|
+
const walk = (items, labels) => {
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
const path = [...labels, item.label];
|
|
157
|
+
entries.set(formOptionKey(item.value), { option: item, path: path.join(separator.value) });
|
|
158
|
+
walk(item.children ?? [], path);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
walk(options.items.value, []);
|
|
162
|
+
return entries;
|
|
163
|
+
});
|
|
164
|
+
const showsPath = computed(() => props.field.type === "cascader" || hasShowPath(props.field));
|
|
165
|
+
function optionOf(item) {
|
|
166
|
+
return optionEntries.value.get(formOptionKey(item.value))?.option;
|
|
167
|
+
}
|
|
168
|
+
function tokenLabel(option) {
|
|
169
|
+
return showsPath.value ? optionEntries.value.get(formOptionKey(option.value))?.path ?? option.label : option.label;
|
|
170
|
+
}
|
|
171
|
+
const chosenOptions = computed(() => {
|
|
172
|
+
const values = Array.isArray(model.value) ? model.value : [model.value];
|
|
173
|
+
return values.flatMap((value) => {
|
|
174
|
+
const option = value === null ? void 0 : optionEntries.value.get(formOptionKey(value));
|
|
175
|
+
return option ? [option.option] : [];
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
const leadingOption = computed(() => {
|
|
179
|
+
const [option] = chosenOptions.value;
|
|
180
|
+
const multiple = usesTree.value ? treeMultiple.value : fieldProps.value.multiple === true;
|
|
181
|
+
return !multiple && option && resolveOptionLeading(option) ? option : void 0;
|
|
182
|
+
});
|
|
150
183
|
const ownedControlUi = computed(() => ({
|
|
151
184
|
...controlProps.value.ui,
|
|
185
|
+
base: mergeFormUiClass(
|
|
186
|
+
controlProps.value.ui?.base,
|
|
187
|
+
leadingOption.value ? leadingVisualPadding(controlProps.value.size) : void 0
|
|
188
|
+
),
|
|
152
189
|
content: mergeFormUiClass(controlProps.value.ui?.content, interactionOwnerClass.value)
|
|
153
190
|
}));
|
|
154
191
|
watch(treeSelectOpen, (open) => {
|
|
@@ -402,6 +439,7 @@ function isOptionValue(value) {
|
|
|
402
439
|
@click.stop
|
|
403
440
|
@select="selectRadioTreeItem(item)"
|
|
404
441
|
/>
|
|
442
|
+
<FormOptionLeading :option="optionOf(item)" />
|
|
405
443
|
<span :class="ui.linkLabel()">{{ item.label }}</span>
|
|
406
444
|
<button
|
|
407
445
|
v-if="isExpandable(item)"
|
|
@@ -460,7 +498,25 @@ function isOptionValue(value) {
|
|
|
460
498
|
}"
|
|
461
499
|
@blur="handleBlur"
|
|
462
500
|
>
|
|
463
|
-
|
|
501
|
+
<FormOptionTokens
|
|
502
|
+
v-if="treeMultiple && chosenOptions.length"
|
|
503
|
+
:options="chosenOptions"
|
|
504
|
+
:label-of="tokenLabel"
|
|
505
|
+
/>
|
|
506
|
+
<span
|
|
507
|
+
v-else
|
|
508
|
+
:class="
|
|
509
|
+
mergeFormUiClass(
|
|
510
|
+
'flex min-w-0 flex-1 items-center gap-1.5 text-left',
|
|
511
|
+
formUi.ui.value.treeSelect?.ui?.triggerLabel
|
|
512
|
+
)
|
|
513
|
+
"
|
|
514
|
+
>
|
|
515
|
+
<FormOptionLeading v-if="leadingOption" :option="leadingOption" />
|
|
516
|
+
<span :class="chosenOptions.length ? 'truncate' : 'truncate text-dimmed'">
|
|
517
|
+
{{ treeSelectLabel }}
|
|
518
|
+
</span>
|
|
519
|
+
</span>
|
|
464
520
|
</UButton>
|
|
465
521
|
|
|
466
522
|
<template #content>
|
|
@@ -538,6 +594,7 @@ function isOptionValue(value) {
|
|
|
538
594
|
@click.stop
|
|
539
595
|
@select="selectRadioTreeItem(item)"
|
|
540
596
|
/>
|
|
597
|
+
<FormOptionLeading :option="optionOf(item)" />
|
|
541
598
|
<span :class="ui.linkLabel()">{{ item.label }}</span>
|
|
542
599
|
<button
|
|
543
600
|
v-if="isExpandable(item)"
|
|
@@ -610,7 +667,19 @@ function isOptionValue(value) {
|
|
|
610
667
|
:loading="validationPending || options.loading.value"
|
|
611
668
|
:ui="ownedControlUi"
|
|
612
669
|
@blur="handleBlur"
|
|
613
|
-
|
|
670
|
+
>
|
|
671
|
+
<template v-if="leadingOption" #leading="{ ui }">
|
|
672
|
+
<FormOptionLeading
|
|
673
|
+
:option="leadingOption"
|
|
674
|
+
:icon-class="ui.leadingIcon()"
|
|
675
|
+
:avatar-size="ui.itemLeadingAvatarSize()"
|
|
676
|
+
chip-class="size-5"
|
|
677
|
+
/>
|
|
678
|
+
</template>
|
|
679
|
+
<template v-if="fieldProps.multiple && chosenOptions.length" #default>
|
|
680
|
+
<FormOptionTokens :options="chosenOptions" :label-of="tokenLabel" />
|
|
681
|
+
</template>
|
|
682
|
+
</USelectMenu>
|
|
614
683
|
</FormCompositeControl>
|
|
615
684
|
</FormFieldShell>
|
|
616
685
|
</template>
|
|
@@ -9,9 +9,10 @@ import FormOptionItemLabel from "../../components/utils/form-option-item-label.v
|
|
|
9
9
|
import FormOptionLeading from "../../components/utils/form-option-leading.vue";
|
|
10
10
|
import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
|
|
11
11
|
import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
|
|
12
|
+
import FormOptionTokens from "../../components/utils/form-option-tokens.vue";
|
|
12
13
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
13
14
|
import { useOptionMenu } from "../../composables/use-option-menu";
|
|
14
|
-
import { leadingVisualPadding } from "../../utils/options";
|
|
15
|
+
import { formOptionKey, leadingVisualPadding } from "../../utils/options";
|
|
15
16
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
16
17
|
import { mergeFormUiClass } from "../../utils/ui";
|
|
17
18
|
const props = defineProps({
|
|
@@ -76,6 +77,17 @@ const controlUi = computed(() => ({
|
|
|
76
77
|
controlProps.value.ui?.itemDescription
|
|
77
78
|
)
|
|
78
79
|
}));
|
|
80
|
+
const tokenOptions = computed(() => {
|
|
81
|
+
if (!fieldProps.value.multiple || !Array.isArray(model.value)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const chosen = new Map(
|
|
85
|
+
options.selectedItems.value.map((option) => [formOptionKey(option.value), option])
|
|
86
|
+
);
|
|
87
|
+
return model.value.map(
|
|
88
|
+
(value) => chosen.get(formOptionKey(value)) ?? { label: String(value), value }
|
|
89
|
+
);
|
|
90
|
+
});
|
|
79
91
|
const createItem = computed(
|
|
80
92
|
() => options.creatable.value && fieldProps.value.createItem ? fieldProps.value.createItem : false
|
|
81
93
|
);
|
|
@@ -166,6 +178,9 @@ function isOptionValue(value) {
|
|
|
166
178
|
/>
|
|
167
179
|
</span>
|
|
168
180
|
</template>
|
|
181
|
+
<template v-else-if="tokenOptions.length" #default>
|
|
182
|
+
<FormOptionTokens :options="tokenOptions" />
|
|
183
|
+
</template>
|
|
169
184
|
|
|
170
185
|
<template #item-leading="{ item }">
|
|
171
186
|
<FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import USwitch from "@nuxt/ui/components/Switch.vue";
|
|
3
3
|
import { computed, useId } from "vue";
|
|
4
4
|
import FormFieldShell from "../../components/renderer/form-field-shell.vue";
|
|
5
|
+
import FormOptionChoiceLabel from "../../components/utils/form-option-choice-label.vue";
|
|
5
6
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
6
7
|
import { formOptionKey } from "../../utils/options";
|
|
7
8
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
@@ -40,7 +41,7 @@ function isOptionValue(value) {
|
|
|
40
41
|
<div
|
|
41
42
|
role="group"
|
|
42
43
|
class="grid gap-3"
|
|
43
|
-
:class="fieldProps.orientation === 'horizontal' ? 'sm:flex sm:flex-wrap' : ''"
|
|
44
|
+
:class="fieldProps.orientation === 'horizontal' ? 'sm:flex sm:flex-wrap sm:gap-x-7' : ''"
|
|
44
45
|
>
|
|
45
46
|
<USwitch
|
|
46
47
|
v-for="item in items"
|
|
@@ -56,7 +57,11 @@ function isOptionValue(value) {
|
|
|
56
57
|
:unchecked-icon="fieldProps.uncheckedIcon"
|
|
57
58
|
@update:model-value="toggleOption(item.value, $event === true)"
|
|
58
59
|
@blur="handleBlur"
|
|
59
|
-
|
|
60
|
+
>
|
|
61
|
+
<template #label>
|
|
62
|
+
<FormOptionChoiceLabel :option="item" :selected="model.includes(item.value)" />
|
|
63
|
+
</template>
|
|
64
|
+
</USwitch>
|
|
60
65
|
</div>
|
|
61
66
|
</FormFieldShell>
|
|
62
67
|
</template>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ui-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
|
|
5
5
|
"homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
|
|
6
6
|
"bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",
|