nuxt-ui-tools 1.16.4 → 1.16.6

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  "configKey": "nuxtUiTools",
6
6
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
7
7
  "name": "nuxt-ui-tools",
8
- "version": "1.16.4",
8
+ "version": "1.16.6",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -1,8 +1,13 @@
1
+ import type { FormConfirmHandler } from '../../types/index.js';
2
+ type __VLS_Props = {
3
+ /** Asks before discarding work, such as leaving a dirty form. Defaults to the native dialog. */
4
+ confirm?: FormConfirmHandler;
5
+ };
1
6
  declare var __VLS_1: {};
2
7
  type __VLS_Slots = {} & {
3
8
  default?: (props: typeof __VLS_1) => any;
4
9
  };
5
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
10
+ declare const __VLS_base: 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>;
6
11
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
12
  declare const _default: typeof __VLS_export;
8
13
  export default _default;
@@ -1,8 +1,13 @@
1
1
  <script setup>
2
2
  import { provideFormApi } from "../../composables/use-form-api";
3
+ import { nativeConfirm, provideFormConfirm } from "../../composables/use-form-confirm";
3
4
  import FormOverlayHost from "./form-overlay-host.vue";
5
+ const props = defineProps({
6
+ confirm: { type: Function, required: false }
7
+ });
4
8
  const formApi = provideFormApi();
5
9
  const { formInstances } = formApi;
10
+ provideFormConfirm((request) => props.confirm ? props.confirm(request) : nativeConfirm(request));
6
11
  </script>
7
12
 
8
13
  <template>
@@ -1,8 +1,13 @@
1
+ import type { FormConfirmHandler } from '../../types/index.js';
2
+ type __VLS_Props = {
3
+ /** Asks before discarding work, such as leaving a dirty form. Defaults to the native dialog. */
4
+ confirm?: FormConfirmHandler;
5
+ };
1
6
  declare var __VLS_1: {};
2
7
  type __VLS_Slots = {} & {
3
8
  default?: (props: typeof __VLS_1) => any;
4
9
  };
5
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
10
+ declare const __VLS_base: 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>;
6
11
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
12
  declare const _default: typeof __VLS_export;
8
13
  export default _default;
@@ -0,0 +1,5 @@
1
+ import type { FormConfirmHandler, FormConfirmRequest } from '../types/index.js';
2
+ export declare function provideFormConfirm(handler: FormConfirmHandler): void;
3
+ /** Asks the app's confirm handler, or the native dialog when no provider supplies one. */
4
+ export declare function useFormConfirm(): (request: FormConfirmRequest) => Promise<boolean>;
5
+ export declare function nativeConfirm(request: FormConfirmRequest): boolean;
@@ -0,0 +1,14 @@
1
+ import { inject, provide } from "vue";
2
+ const formConfirmKey = Symbol("nuxt-ui-tools-form-confirm");
3
+ export function provideFormConfirm(handler) {
4
+ provide(formConfirmKey, handler);
5
+ }
6
+ export function useFormConfirm() {
7
+ const handler = inject(formConfirmKey, nativeConfirm);
8
+ return async function confirm(request) {
9
+ return await handler(request) === true;
10
+ };
11
+ }
12
+ export function nativeConfirm(request) {
13
+ return window.confirm(request.message);
14
+ }
@@ -11,7 +11,7 @@ export declare function useFormPage(params: {
11
11
  hash: () => boolean;
12
12
  }): {
13
13
  active: import("vue").Ref<string | undefined, string | undefined>;
14
- cancel: () => void;
14
+ cancel: () => Promise<void>;
15
15
  dirtyCheck: import("vue").ComputedRef<boolean>;
16
16
  items: import("vue").ComputedRef<{
17
17
  entry: import("../types/index.js").FormPageSectionEntry;
@@ -23,7 +23,7 @@ export declare function useFormPage(params: {
23
23
  remaining: import("vue").ComputedRef<number>;
24
24
  resetSection: (key: string) => Promise<void>;
25
25
  root: {
26
- cancel: () => void;
26
+ cancel: () => Promise<void>;
27
27
  contextError: import("vue").ComputedRef<string | undefined>;
28
28
  contextLoading: import("vue").ComputedRef<boolean>;
29
29
  contextPending: import("vue").ComputedRef<boolean>;
@@ -45,7 +45,7 @@ export declare function useFormPage(params: {
45
45
  };
46
46
  export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
47
47
  active: import("vue").Ref<string | undefined, string | undefined>;
48
- cancel: () => void;
48
+ cancel: () => Promise<void>;
49
49
  dirtyCheck: import("vue").ComputedRef<boolean>;
50
50
  items: import("vue").ComputedRef<{
51
51
  entry: import("../types/index.js").FormPageSectionEntry;
@@ -57,7 +57,7 @@ export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
57
57
  remaining: import("vue").ComputedRef<number>;
58
58
  resetSection: (key: string) => Promise<void>;
59
59
  root: {
60
- cancel: () => void;
60
+ cancel: () => Promise<void>;
61
61
  contextError: import("vue").ComputedRef<string | undefined>;
62
62
  contextLoading: import("vue").ComputedRef<boolean>;
63
63
  contextPending: import("vue").ComputedRef<boolean>;
@@ -80,7 +80,7 @@ export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
80
80
  /** The page a part renders in. Parts only work inside `FormPage`. */
81
81
  export declare function useFormPageContext(): {
82
82
  active: import("vue").Ref<string | undefined, string | undefined>;
83
- cancel: () => void;
83
+ cancel: () => Promise<void>;
84
84
  dirtyCheck: import("vue").ComputedRef<boolean>;
85
85
  items: import("vue").ComputedRef<{
86
86
  entry: import("../types/index.js").FormPageSectionEntry;
@@ -92,7 +92,7 @@ export declare function useFormPageContext(): {
92
92
  remaining: import("vue").ComputedRef<number>;
93
93
  resetSection: (key: string) => Promise<void>;
94
94
  root: {
95
- cancel: () => void;
95
+ cancel: () => Promise<void>;
96
96
  contextError: import("vue").ComputedRef<string | undefined>;
97
97
  contextLoading: import("vue").ComputedRef<boolean>;
98
98
  contextPending: import("vue").ComputedRef<boolean>;
@@ -18,7 +18,7 @@ export interface UseFormRootParams {
18
18
  * and owns submit and cancel. `<NutForm>` and `FormPage` both render from it.
19
19
  */
20
20
  export declare function useFormRoot(params: UseFormRootParams): {
21
- cancel: () => void;
21
+ cancel: () => Promise<void>;
22
22
  contextError: import("vue").ComputedRef<string | undefined>;
23
23
  contextLoading: import("vue").ComputedRef<boolean>;
24
24
  contextPending: import("vue").ComputedRef<boolean>;
@@ -14,9 +14,9 @@ import { isString, isUndefined, stringArray } from "../utils/predicate.js";
14
14
  import { collectFormFieldsPaths } from "../utils/state.js";
15
15
  import { resolveFormText } from "../utils/text.js";
16
16
  import { mergeFormUi, resolveAppFormUi } from "../utils/ui.js";
17
+ import { useFormConfirm } from "./use-form-confirm.js";
17
18
  import { provideFormRuntime, useFormRuntime } from "./use-form-runtime.js";
18
19
  import { provideFormUi } from "./use-form-ui.js";
19
- const DEFAULT_DIRTY_NAVIGATION_MESSAGE = "You have unsaved changes. Close this form?";
20
20
  export function useFormRoot(params) {
21
21
  const router = useRouter();
22
22
  const { t } = useUiToolsLocale();
@@ -37,6 +37,14 @@ export function useFormRoot(params) {
37
37
  const ownedRuntime = params.runtime?.();
38
38
  const runtime = ownedRuntime ?? useFormRuntime({ input, schema, syncInput, validationMode });
39
39
  provideFormRuntime(runtime);
40
+ const confirm = useFormConfirm();
41
+ let leaveConfirmed = false;
42
+ watch(
43
+ () => runtime.dirtyPaths.value,
44
+ () => {
45
+ leaveConfirmed = false;
46
+ }
47
+ );
40
48
  if (!ownedRuntime) {
41
49
  watch(
42
50
  params.form,
@@ -74,10 +82,10 @@ export function useFormRoot(params) {
74
82
  if (runtime.actionPending.value === "submit" || isSameDocument(to, from)) {
75
83
  return true;
76
84
  }
77
- if (!shouldConfirmDirtyNavigation()) {
85
+ if (leaveConfirmed || !shouldConfirmDirtyNavigation()) {
78
86
  return true;
79
87
  }
80
- return window.confirm(getDirtyNavigationMessage());
88
+ return confirmLeave();
81
89
  });
82
90
  onBeforeUnmount(removeRouteGuard);
83
91
  const contextPending = computed(
@@ -117,12 +125,19 @@ export function useFormRoot(params) {
117
125
  params.onSubmitted(runtime.output.value, result);
118
126
  }
119
127
  }
120
- function cancel() {
121
- if (shouldConfirmDirtyNavigation() && !window.confirm(getDirtyNavigationMessage())) {
128
+ async function cancel() {
129
+ if (!leaveConfirmed && shouldConfirmDirtyNavigation() && !await confirmLeave()) {
122
130
  return;
123
131
  }
124
132
  params.onCancelled(runtime.output.value);
125
133
  }
134
+ async function confirmLeave() {
135
+ leaveConfirmed = await confirm({
136
+ kind: "unsaved-changes",
137
+ message: getDirtyNavigationMessage()
138
+ });
139
+ return leaveConfirmed;
140
+ }
126
141
  function shouldConfirmDirtyNavigation() {
127
142
  if (!runtime.isDirty.value) {
128
143
  return false;
@@ -142,10 +157,10 @@ export function useFormRoot(params) {
142
157
  function getDirtyNavigationMessage() {
143
158
  const config = getSchemaDirtyNavigation(schema.value);
144
159
  if (!isRecord(config)) {
145
- return DEFAULT_DIRTY_NAVIGATION_MESSAGE;
160
+ return t("form.confirm.unsavedChanges");
146
161
  }
147
162
  const message = Object.getOwnPropertyDescriptor(config, "message")?.value;
148
- return resolveFormText(message) ?? DEFAULT_DIRTY_NAVIGATION_MESSAGE;
163
+ return resolveFormText(message) ?? t("form.confirm.unsavedChanges");
149
164
  }
150
165
  async function focusFirstField() {
151
166
  const parentPath = runtime.currentStepRoot.value ? [runtime.currentStepRoot.value] : [];
@@ -87,8 +87,8 @@ function toggle(index) {
87
87
  function addItem() {
88
88
  expand(array.addItem());
89
89
  }
90
- function removeItem(index) {
91
- if (!array.removeItem(index)) {
90
+ async function removeItem(index) {
91
+ if (!await array.removeItem(index)) {
92
92
  return;
93
93
  }
94
94
  expanded.value = expanded.value.filter((candidate) => candidate !== index).map((candidate) => candidate > index ? candidate - 1 : candidate);
@@ -75,13 +75,13 @@ function addItem() {
75
75
  tabTransitionDirection.value = "forward";
76
76
  activeIndex.value = array.addItem();
77
77
  }
78
- function removeItem(index) {
78
+ async function removeItem(index) {
79
79
  const currentActiveIndex = activeIndex.value;
80
80
  const hasNextItem = index + 1 < items.value.length;
81
81
  if (currentActiveIndex === index) {
82
82
  tabTransitionDirection.value = hasNextItem ? "forward" : "backward";
83
83
  }
84
- if (!array.removeItem(index)) {
84
+ if (!await array.removeItem(index)) {
85
85
  return;
86
86
  }
87
87
  if (currentActiveIndex < index) {
@@ -41,7 +41,7 @@ export declare function useFormArrayItems(field: () => FormArrayItemsField, path
41
41
  itemPath: (index: number) => string[];
42
42
  itemRenderKey: (item: FormObject, index: number) => string;
43
43
  items: import("vue").ComputedRef<readonly import("../../../shared/index.js").GenericObject[]>;
44
- removeItem: (index: number) => boolean;
44
+ removeItem: (index: number) => Promise<boolean>;
45
45
  removeItemLabel: (item: FormObject, index: number) => string;
46
46
  resolveAction: (action: FormArrayAction | undefined, index: number) => (import("./types.js").FormArrayActionCondition<{}, {}> | undefined) & boolean;
47
47
  runCustomAction: (index: number, actionIndex: number) => Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useUiToolsLocale } from "../../../i18n/use-locale.js";
3
3
  import { useResolvedFieldProps } from "../../composables/use-field-control.js";
4
+ import { useFormConfirm } from "../../composables/use-form-confirm.js";
4
5
  import { useFormRuntimeContext } from "../../composables/use-form-runtime.js";
5
6
  import { useFormUi } from "../../composables/use-form-ui.js";
6
7
  import { syncFormArrayItems } from "../../utils/array.js";
@@ -18,6 +19,7 @@ export function useFormArrayItems(field, path) {
18
19
  const formUi = useFormUi();
19
20
  const fieldProps = useResolvedFieldProps(field, path);
20
21
  const { t } = useUiToolsLocale();
22
+ const confirm = useFormConfirm();
21
23
  const itemKeys = /* @__PURE__ */ new WeakMap();
22
24
  let nextItemKey = 0;
23
25
  const items = computed(() => {
@@ -69,16 +71,16 @@ export function useFormArrayItems(field, path) {
69
71
  updateItems([...items.value, createItem(index, variantKey)]);
70
72
  return index;
71
73
  }
72
- function confirmRemoval() {
74
+ async function confirmRemoval() {
73
75
  const current = field();
74
76
  if (!current.confirmDelete) {
75
77
  return true;
76
78
  }
77
79
  const message = resolveFormBoundaryText(current.confirmDelete) ?? t("form.fields.array.confirmDelete");
78
- return window.confirm(message);
80
+ return confirm({ kind: "remove-item", message });
79
81
  }
80
- function removeItem(index) {
81
- if (!confirmRemoval()) {
82
+ async function removeItem(index) {
83
+ if (!await confirmRemoval()) {
82
84
  return false;
83
85
  }
84
86
  updateItems(items.value.filter((_, itemIndex) => itemIndex !== index));
@@ -6,6 +6,7 @@ import { useUiToolsLocale } from "../../../i18n/use-locale";
6
6
  import FormFieldRenderer from "../../components/renderer/form-field-renderer.vue";
7
7
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
8
8
  import { useFieldControl } from "../../composables/use-field-control";
9
+ import { useFormConfirm } from "../../composables/use-form-confirm";
9
10
  import { useFormUi } from "../../composables/use-form-ui";
10
11
  import { formOptionKey, normalizeOptionItem, resolveOptionSource } from "../../utils/options";
11
12
  import { isRecord } from "../../utils/path";
@@ -34,6 +35,7 @@ const VueDraggable = defineAsyncComponent(async () => {
34
35
  });
35
36
  const formUi = useFormUi();
36
37
  const { t } = useUiToolsLocale();
38
+ const confirm = useFormConfirm();
37
39
  const ui = computed(() => formUi.ui.value.arrayPrimitive?.ui);
38
40
  const values = computed(() => {
39
41
  const value = form.getValue(props.path);
@@ -159,10 +161,10 @@ async function addItem() {
159
161
  await nextTick();
160
162
  await form.focusField(itemPath(index));
161
163
  }
162
- function removeItem(index) {
164
+ async function removeItem(index) {
163
165
  if (!isPending(index) && props.field.confirmDelete !== false) {
164
166
  const message = resolveFormBoundaryText(props.field.confirmDelete) ?? t("form.fields.array.confirmDelete");
165
- if (!window.confirm(message)) {
167
+ if (!await confirm({ kind: "remove-item", message })) {
166
168
  return;
167
169
  }
168
170
  }
@@ -31,6 +31,7 @@ const {
31
31
  { omit: ["createItem", "max", "searchable", "clearable"] }
32
32
  );
33
33
  const searchTerm = ref("");
34
+ const open = ref(false);
34
35
  const model = computed({
35
36
  get: () => {
36
37
  const value = form.getValue(props.path);
@@ -76,8 +77,9 @@ watch(searchTerm, (term) => {
76
77
  options.setSearch(term);
77
78
  }
78
79
  });
79
- function handleOpen(open) {
80
- if (open) {
80
+ function handleOpen(next) {
81
+ open.value = next;
82
+ if (next) {
81
83
  options.activate();
82
84
  }
83
85
  }
@@ -85,7 +87,11 @@ async function handleCreate(label) {
85
87
  await options.create(label);
86
88
  }
87
89
  async function handleCreateAction() {
88
- await options.create(searchTerm.value.trim() || void 0);
90
+ const label = searchTerm.value.trim() || void 0;
91
+ if (!fieldProps.value.multiple) {
92
+ open.value = false;
93
+ }
94
+ await options.create(label);
89
95
  }
90
96
  async function handleNativeCreate(label) {
91
97
  const normalizedLabel = label.trim();
@@ -107,6 +113,7 @@ function isOptionValue(value) {
107
113
  v-if="bare"
108
114
  v-model="model"
109
115
  v-model:search-term="searchTerm"
116
+ :open="open"
110
117
  v-bind="controlProps"
111
118
  class="w-full"
112
119
  value-key="value"
@@ -152,6 +159,7 @@ function isOptionValue(value) {
152
159
  <USelectMenu
153
160
  v-model="model"
154
161
  v-model:search-term="searchTerm"
162
+ :open="open"
155
163
  v-bind="controlProps"
156
164
  class="w-full"
157
165
  value-key="value"
@@ -0,0 +1,9 @@
1
+ /** A question the form asks before discarding work. */
2
+ export interface FormConfirmRequest {
3
+ /** `unsaved-changes` before leaving a dirty form; `remove-item` before removing an array item. */
4
+ kind: 'unsaved-changes' | 'remove-item';
5
+ /** Localized message: the schema's text or the engine default. */
6
+ message: string;
7
+ }
8
+ /** App-provided confirmation, such as a modal. Resolves `true` to go ahead. */
9
+ export type FormConfirmHandler = (request: FormConfirmRequest) => boolean | Promise<boolean>;
File without changes
@@ -1,6 +1,7 @@
1
1
  export type * from './api';
2
2
  export type * from './actions';
3
3
  export type * from './callbacks';
4
+ export type * from './confirm';
4
5
  export type * from './context';
5
6
  export type * from './controller';
6
7
  export type * from './dependencies';
@@ -193,6 +193,9 @@ export default defineUiToolsLocale({
193
193
  untitled: "Untitled file"
194
194
  },
195
195
  form: {
196
+ confirm: {
197
+ unsavedChanges: "You have unsaved changes. Discard them?"
198
+ },
196
199
  actions: {
197
200
  cancelButton: "Cancel",
198
201
  nextButton: "Next",
@@ -193,6 +193,9 @@ export default defineUiToolsLocale({
193
193
  untitled: "Fichier sans nom"
194
194
  },
195
195
  form: {
196
+ confirm: {
197
+ unsavedChanges: "Vous avez des modifications non enregistr\xE9es. Les abandonner ?"
198
+ },
196
199
  actions: {
197
200
  cancelButton: "Annuler",
198
201
  nextButton: "Suivant",
@@ -411,6 +411,10 @@ export interface UiToolsSpreadsheetMessages {
411
411
  };
412
412
  }
413
413
  export interface UiToolsFormMessages {
414
+ /** Confirmations asked before discarding work. */
415
+ confirm: {
416
+ unsavedChanges: string;
417
+ };
414
418
  actions: {
415
419
  nextButton: string;
416
420
  prevButton: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.16.4",
3
+ "version": "1.16.6",
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",