nuxt-ui-tools 1.3.0 → 1.5.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.
Files changed (94) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +10 -2
  3. package/dist/runtime/dashboard/composables/use-dashboard-remote-options.js +8 -3
  4. package/dist/runtime/dashboard/types/filters.d.ts +7 -1
  5. package/dist/runtime/dashboard/utils/builders/dashboard-filters.js +19 -2
  6. package/dist/runtime/form/components/actions/form-actions.vue +13 -127
  7. package/dist/runtime/form/components/page/form-page-actions.d.vue.ts +3 -0
  8. package/dist/runtime/form/components/page/form-page-actions.vue +40 -0
  9. package/dist/runtime/form/components/page/form-page-actions.vue.d.ts +3 -0
  10. package/dist/runtime/form/components/page/form-page-header.d.vue.ts +24 -0
  11. package/dist/runtime/form/components/page/form-page-header.vue +99 -0
  12. package/dist/runtime/form/components/page/form-page-header.vue.d.ts +24 -0
  13. package/dist/runtime/form/components/page/form-page-navigation.d.vue.ts +34 -0
  14. package/dist/runtime/form/components/page/form-page-navigation.vue +229 -0
  15. package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +34 -0
  16. package/dist/runtime/form/components/page/form-page-section.d.vue.ts +19 -0
  17. package/dist/runtime/form/components/page/form-page-section.vue +114 -0
  18. package/dist/runtime/form/components/page/form-page-section.vue.d.ts +19 -0
  19. package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +21 -0
  20. package/dist/runtime/form/components/page/form-page-sections.vue +75 -0
  21. package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +21 -0
  22. package/dist/runtime/form/components/page/form-page.d.vue.ts +80 -0
  23. package/dist/runtime/form/components/page/form-page.vue +91 -0
  24. package/dist/runtime/form/components/page/form-page.vue.d.ts +80 -0
  25. package/dist/runtime/form/components/renderer/form-field-shell.vue +7 -11
  26. package/dist/runtime/form/components/root/form.vue +25 -194
  27. package/dist/runtime/form/composables/use-field-options.js +19 -0
  28. package/dist/runtime/form/composables/use-form-action-buttons.d.ts +32 -0
  29. package/dist/runtime/form/composables/use-form-action-buttons.js +89 -0
  30. package/dist/runtime/form/composables/use-form-actions.d.ts +8 -5
  31. package/dist/runtime/form/composables/use-form-actions.js +1 -1
  32. package/dist/runtime/form/composables/use-form-page-scroll.d.ts +22 -0
  33. package/dist/runtime/form/composables/use-form-page-scroll.js +178 -0
  34. package/dist/runtime/form/composables/use-form-page.d.ts +114 -0
  35. package/dist/runtime/form/composables/use-form-page.js +83 -0
  36. package/dist/runtime/form/composables/use-form-root.d.ts +34 -0
  37. package/dist/runtime/form/composables/use-form-root.js +171 -0
  38. package/dist/runtime/form/composables/use-form-runtime.js +13 -4
  39. package/dist/runtime/form/composables/use-form.js +2 -22
  40. package/dist/runtime/form/composables/use-remote-field-options.js +21 -2
  41. package/dist/runtime/form/fields/checkbox-card/component.vue +28 -19
  42. package/dist/runtime/form/fields/checkbox-card/types.d.ts +2 -5
  43. package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +17 -0
  44. package/dist/runtime/form/fields/choice-card/choice-card-label.vue +50 -0
  45. package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +17 -0
  46. package/dist/runtime/form/fields/choice-card/types.d.ts +31 -0
  47. package/dist/runtime/form/fields/choice-card/types.js +0 -0
  48. package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +22 -0
  49. package/dist/runtime/form/fields/choice-card/use-choice-card.js +52 -0
  50. package/dist/runtime/form/fields/radio-card/component.vue +17 -18
  51. package/dist/runtime/form/fields/radio-card/types.d.ts +2 -4
  52. package/dist/runtime/form/index.d.ts +1 -1
  53. package/dist/runtime/form/index.js +7 -1
  54. package/dist/runtime/form/schema/index.d.ts +1 -1
  55. package/dist/runtime/form/schema/index.js +1 -0
  56. package/dist/runtime/form/schema/page.d.ts +50 -0
  57. package/dist/runtime/form/schema/page.js +25 -0
  58. package/dist/runtime/form/types/index.d.ts +1 -0
  59. package/dist/runtime/form/types/options.d.ts +22 -2
  60. package/dist/runtime/form/types/page.d.ts +172 -0
  61. package/dist/runtime/form/types/page.js +0 -0
  62. package/dist/runtime/form/types/ui.d.ts +60 -0
  63. package/dist/runtime/form/utils/controls.d.ts +15 -0
  64. package/dist/runtime/form/utils/controls.js +38 -0
  65. package/dist/runtime/form/utils/layout.js +3 -1
  66. package/dist/runtime/form/utils/page.d.ts +20 -0
  67. package/dist/runtime/form/utils/page.js +107 -0
  68. package/dist/runtime/form/utils/state.d.ts +2 -0
  69. package/dist/runtime/form/utils/state.js +1 -1
  70. package/dist/runtime/form/utils/ui.js +1 -0
  71. package/dist/runtime/i18n/locales/en.js +18 -0
  72. package/dist/runtime/i18n/locales/fr.js +18 -0
  73. package/dist/runtime/i18n/types.d.ts +29 -0
  74. package/dist/runtime/shared/index.d.ts +1 -0
  75. package/dist/runtime/shared/index.js +1 -0
  76. package/dist/runtime/shared/types/query.d.ts +5 -1
  77. package/dist/runtime/shared/types/remote-options.d.ts +56 -0
  78. package/dist/runtime/shared/utils/define-remote-options.d.ts +23 -0
  79. package/dist/runtime/shared/utils/define-remote-options.js +24 -0
  80. package/dist/runtime/shared/utils/remote-options-query.d.ts +6 -0
  81. package/dist/runtime/shared/utils/remote-options-query.js +18 -0
  82. package/dist/runtime/table/components/data-list/data-list-filter-tags.d.vue.ts +1 -1
  83. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue +1 -1
  84. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue.d.ts +1 -1
  85. package/dist/runtime/table/components/filters/panel/filter-panel.vue +14 -2
  86. package/dist/runtime/table/components/filters/shared/filter-option-multiple-list.vue +2 -2
  87. package/dist/runtime/table/components/filters/shared/filter-option-single-list.vue +2 -2
  88. package/dist/runtime/table/components/filters/shared/filter-option-tree-content.vue +5 -5
  89. package/dist/runtime/table/components/filters/shared/filter-trigger-tag.vue +7 -1
  90. package/dist/runtime/table/composables/use-table-filter-selected-options.js +16 -1
  91. package/dist/runtime/table/types/filters.d.ts +7 -0
  92. package/dist/runtime/table/types/remote-options.d.ts +3 -16
  93. package/dist/runtime/table/utils/remote-table-options.js +37 -37
  94. package/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ import type { ComputedRef } from 'vue';
2
+ import type { FormPageScrollOptions, FormPageSectionState } from '../types/index.js';
3
+ /**
4
+ * Scroll behavior of a form page: which section is in view (a scrollspy on the nearest scrolling
5
+ * ancestor), scrolling to a section, and the URL hash that records and restores it.
6
+ *
7
+ * A section counts as reached once its top passes the line its `scroll-margin-top` defines, the
8
+ * same offset `scrollIntoView` honors, so the pinned header and the scrollspy agree. The section
9
+ * of an entry clicked in the navigation, and the one holding the field that takes focus (such as
10
+ * the first invalid field on submit), stay current through the scroll they cause, until the page
11
+ * is scrolled again.
12
+ */
13
+ export declare function useFormPageScroll(params: {
14
+ sections: ComputedRef<readonly FormPageSectionState[]>;
15
+ /** Element the scroll container is looked up from: the page itself, or an ancestor. */
16
+ root: () => HTMLElement | null;
17
+ hash: () => boolean;
18
+ }): {
19
+ active: import("vue").Ref<string | undefined, string | undefined>;
20
+ register: (key: string, element: HTMLElement) => () => void;
21
+ scrollTo: (key: string, options?: FormPageScrollOptions) => boolean;
22
+ };
@@ -0,0 +1,178 @@
1
+ import { nextTick, onBeforeUnmount, onMounted, ref } from "vue";
2
+ const SCROLL_SETTLE_MS = 120;
3
+ const ACTIVATION_SLACK = 8;
4
+ export function useFormPageScroll(params) {
5
+ const elements = /* @__PURE__ */ new Map();
6
+ const active = ref();
7
+ let scroller;
8
+ let focusRoot = null;
9
+ let target;
10
+ let pendingHash;
11
+ let settleTimer;
12
+ let holdFrame = 0;
13
+ let frame = 0;
14
+ function register(key, element) {
15
+ elements.set(key, element);
16
+ if (pendingHash === key) {
17
+ pendingHash = void 0;
18
+ void nextTick(() => scrollTo(key, { behavior: "instant", hash: false }));
19
+ } else {
20
+ schedule();
21
+ }
22
+ return () => {
23
+ if (elements.get(key) === element) {
24
+ elements.delete(key);
25
+ }
26
+ schedule();
27
+ };
28
+ }
29
+ function scrollTo(key, options = {}) {
30
+ const element = elements.get(key);
31
+ if (!element) {
32
+ return false;
33
+ }
34
+ hold(key);
35
+ element.scrollIntoView?.({
36
+ behavior: options.behavior ?? (prefersReducedMotion() ? "instant" : "smooth"),
37
+ block: "start"
38
+ });
39
+ if (options.focus) {
40
+ element.querySelector("[data-form-page-section-title]")?.focus({ preventScroll: true });
41
+ }
42
+ if (options.hash ?? params.hash()) {
43
+ writeHash(key);
44
+ }
45
+ return true;
46
+ }
47
+ function hold(key) {
48
+ target = key;
49
+ active.value = key;
50
+ clearTimeout(settleTimer);
51
+ cancelAnimationFrame(holdFrame);
52
+ holdFrame = requestAnimationFrame(() => {
53
+ holdFrame = requestAnimationFrame(() => {
54
+ holdFrame = 0;
55
+ settleSoon();
56
+ });
57
+ });
58
+ }
59
+ function settleSoon() {
60
+ clearTimeout(settleTimer);
61
+ settleTimer = setTimeout(() => {
62
+ target = void 0;
63
+ }, SCROLL_SETTLE_MS);
64
+ }
65
+ function onScroll() {
66
+ if (target) {
67
+ settleSoon();
68
+ return;
69
+ }
70
+ schedule();
71
+ }
72
+ function onFocusIn(event) {
73
+ const section = event.target instanceof Element ? event.target.closest("[data-form-page-section]") : null;
74
+ const key = section?.dataset.formPageSection;
75
+ if (key && elements.get(key) === section) {
76
+ hold(key);
77
+ }
78
+ }
79
+ function schedule() {
80
+ if (frame) {
81
+ return;
82
+ }
83
+ frame = requestAnimationFrame(() => {
84
+ frame = 0;
85
+ update();
86
+ });
87
+ }
88
+ function update() {
89
+ if (target) {
90
+ return;
91
+ }
92
+ const next = findActive();
93
+ if (next !== void 0) {
94
+ active.value = next;
95
+ }
96
+ }
97
+ function findActive() {
98
+ const keys = params.sections.value.map((section) => section.key).filter((key) => elements.has(key));
99
+ if (!keys.length) {
100
+ return void 0;
101
+ }
102
+ if (isScrolledToEnd()) {
103
+ return keys.at(-1);
104
+ }
105
+ const line = scroller instanceof HTMLElement ? scroller.getBoundingClientRect().top : 0;
106
+ let current = keys[0];
107
+ for (const key of keys) {
108
+ const element = elements.get(key);
109
+ if (!element) {
110
+ continue;
111
+ }
112
+ const margin = Number.parseFloat(getComputedStyle(element).scrollMarginTop) || 0;
113
+ if (element.getBoundingClientRect().top - margin > line + ACTIVATION_SLACK) {
114
+ break;
115
+ }
116
+ current = key;
117
+ }
118
+ return current;
119
+ }
120
+ function isScrolledToEnd() {
121
+ const box = scroller instanceof HTMLElement ? scroller : document.scrollingElement;
122
+ if (!box || box.scrollHeight <= box.clientHeight) {
123
+ return false;
124
+ }
125
+ return box.scrollTop + box.clientHeight >= box.scrollHeight - 1;
126
+ }
127
+ function onHashChange() {
128
+ const key = readHash();
129
+ if (key && elements.has(key)) {
130
+ scrollTo(key, { hash: false });
131
+ }
132
+ }
133
+ onMounted(() => {
134
+ focusRoot = params.root();
135
+ focusRoot?.addEventListener("focusin", onFocusIn);
136
+ scroller = findScroller(focusRoot);
137
+ scroller.addEventListener("scroll", onScroll, { passive: true });
138
+ window.addEventListener("hashchange", onHashChange);
139
+ const key = params.hash() ? readHash() : void 0;
140
+ if (key && elements.has(key)) {
141
+ void nextTick(() => scrollTo(key, { behavior: "instant", hash: false }));
142
+ } else {
143
+ pendingHash = key;
144
+ schedule();
145
+ }
146
+ });
147
+ onBeforeUnmount(() => {
148
+ focusRoot?.removeEventListener("focusin", onFocusIn);
149
+ scroller?.removeEventListener("scroll", onScroll);
150
+ window.removeEventListener("hashchange", onHashChange);
151
+ clearTimeout(settleTimer);
152
+ cancelAnimationFrame(holdFrame);
153
+ if (frame) {
154
+ cancelAnimationFrame(frame);
155
+ }
156
+ });
157
+ return { active, register, scrollTo };
158
+ }
159
+ function findScroller(element) {
160
+ for (let node = element; node; node = node.parentElement) {
161
+ if (/auto|scroll/u.test(getComputedStyle(node).overflowY)) {
162
+ return node;
163
+ }
164
+ }
165
+ return window;
166
+ }
167
+ function prefersReducedMotion() {
168
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
169
+ }
170
+ function readHash() {
171
+ const hash = window.location.hash.slice(1);
172
+ return hash ? decodeURIComponent(hash) : void 0;
173
+ }
174
+ function writeHash(key) {
175
+ const url = new URL(window.location.href);
176
+ url.hash = key;
177
+ window.history.replaceState(window.history.state, "", url);
178
+ }
@@ -0,0 +1,114 @@
1
+ import type { FormPageSectionState } from '../types/index.js';
2
+ import type { useFormRoot } from './use-form-root.js';
3
+ /**
4
+ * State shared by a form page and its parts: the visible sections and where each stands, the
5
+ * section in view, the navigation summary, and the actions a part can take on a section.
6
+ */
7
+ export declare function useFormPage(params: {
8
+ root: ReturnType<typeof useFormRoot>;
9
+ /** The page element, where the lookup of the scroll container starts. */
10
+ element: () => HTMLElement | null;
11
+ hash: () => boolean;
12
+ }): {
13
+ active: import("vue").Ref<string | undefined, string | undefined>;
14
+ cancel: () => void;
15
+ dirtyCheck: import("vue").ComputedRef<boolean>;
16
+ items: import("vue").ComputedRef<{
17
+ entry: import("../types/index.js").FormPageSectionEntry;
18
+ section: FormPageSectionState;
19
+ }[]>;
20
+ modified: import("vue").ComputedRef<number>;
21
+ navigationTitle: import("vue").ComputedRef<string | undefined>;
22
+ register: (key: string, element: HTMLElement) => () => void;
23
+ remaining: import("vue").ComputedRef<number>;
24
+ resetSection: (key: string) => Promise<void>;
25
+ root: {
26
+ cancel: () => void;
27
+ contextError: import("vue").ComputedRef<string | undefined>;
28
+ contextLoading: import("vue").ComputedRef<boolean>;
29
+ contextPending: import("vue").ComputedRef<boolean>;
30
+ formUi: {
31
+ controlSize: import("vue").ComputedRef<import("../types/index.js").FormControlSize>;
32
+ density: import("vue").ComputedRef<import("../types/index.js").FormDensity>;
33
+ ui: import("vue").ComputedRef<import("../types/index.js").FormUiConfig>;
34
+ };
35
+ input: import("vue").ComputedRef<import("../../shared/index.js").GenericObject | undefined>;
36
+ refreshContext: () => Promise<void>;
37
+ runtime: import("../types/index.js").FormRuntime;
38
+ schema: import("vue").ComputedRef<{}>;
39
+ submit: () => Promise<void>;
40
+ validationMode: import("vue").ComputedRef<import("../types/index.js").FormValidationMode>;
41
+ };
42
+ scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
43
+ sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
44
+ stickyOffset: import("vue").Ref<number, number>;
45
+ };
46
+ export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
47
+ active: import("vue").Ref<string | undefined, string | undefined>;
48
+ cancel: () => void;
49
+ dirtyCheck: import("vue").ComputedRef<boolean>;
50
+ items: import("vue").ComputedRef<{
51
+ entry: import("../types/index.js").FormPageSectionEntry;
52
+ section: FormPageSectionState;
53
+ }[]>;
54
+ modified: import("vue").ComputedRef<number>;
55
+ navigationTitle: import("vue").ComputedRef<string | undefined>;
56
+ register: (key: string, element: HTMLElement) => () => void;
57
+ remaining: import("vue").ComputedRef<number>;
58
+ resetSection: (key: string) => Promise<void>;
59
+ root: {
60
+ cancel: () => void;
61
+ contextError: import("vue").ComputedRef<string | undefined>;
62
+ contextLoading: import("vue").ComputedRef<boolean>;
63
+ contextPending: import("vue").ComputedRef<boolean>;
64
+ formUi: {
65
+ controlSize: import("vue").ComputedRef<import("../types/index.js").FormControlSize>;
66
+ density: import("vue").ComputedRef<import("../types/index.js").FormDensity>;
67
+ ui: import("vue").ComputedRef<import("../types/index.js").FormUiConfig>;
68
+ };
69
+ input: import("vue").ComputedRef<import("../../shared/index.js").GenericObject | undefined>;
70
+ refreshContext: () => Promise<void>;
71
+ runtime: import("../types/index.js").FormRuntime;
72
+ schema: import("vue").ComputedRef<{}>;
73
+ submit: () => Promise<void>;
74
+ validationMode: import("vue").ComputedRef<import("../types/index.js").FormValidationMode>;
75
+ };
76
+ scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
77
+ sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
78
+ stickyOffset: import("vue").Ref<number, number>;
79
+ };
80
+ /** The page a part renders in. Parts only work inside `FormPage`. */
81
+ export declare function useFormPageContext(): {
82
+ active: import("vue").Ref<string | undefined, string | undefined>;
83
+ cancel: () => void;
84
+ dirtyCheck: import("vue").ComputedRef<boolean>;
85
+ items: import("vue").ComputedRef<{
86
+ entry: import("../types/index.js").FormPageSectionEntry;
87
+ section: FormPageSectionState;
88
+ }[]>;
89
+ modified: import("vue").ComputedRef<number>;
90
+ navigationTitle: import("vue").ComputedRef<string | undefined>;
91
+ register: (key: string, element: HTMLElement) => () => void;
92
+ remaining: import("vue").ComputedRef<number>;
93
+ resetSection: (key: string) => Promise<void>;
94
+ root: {
95
+ cancel: () => void;
96
+ contextError: import("vue").ComputedRef<string | undefined>;
97
+ contextLoading: import("vue").ComputedRef<boolean>;
98
+ contextPending: import("vue").ComputedRef<boolean>;
99
+ formUi: {
100
+ controlSize: import("vue").ComputedRef<import("../types/index.js").FormControlSize>;
101
+ density: import("vue").ComputedRef<import("../types/index.js").FormDensity>;
102
+ ui: import("vue").ComputedRef<import("../types/index.js").FormUiConfig>;
103
+ };
104
+ input: import("vue").ComputedRef<import("../../shared/index.js").GenericObject | undefined>;
105
+ refreshContext: () => Promise<void>;
106
+ runtime: import("../types/index.js").FormRuntime;
107
+ schema: import("vue").ComputedRef<{}>;
108
+ submit: () => Promise<void>;
109
+ validationMode: import("vue").ComputedRef<import("../types/index.js").FormValidationMode>;
110
+ };
111
+ scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
112
+ sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
113
+ stickyOffset: import("vue").Ref<number, number>;
114
+ };
@@ -0,0 +1,83 @@
1
+ import { computed, inject, provide, ref } from "vue";
2
+ import { getSchemaDirtyCheck } from "../utils/controls.js";
3
+ import {
4
+ getFormPageNavigationTitle,
5
+ getFormPageSections,
6
+ isFormPageSectionVisible,
7
+ resolveFormPageSectionState
8
+ } from "../utils/page.js";
9
+ import { cloneFormValue } from "../utils/path.js";
10
+ import { useFormPageScroll } from "./use-form-page-scroll.js";
11
+ export function useFormPage(params) {
12
+ const { root } = params;
13
+ const { runtime } = root;
14
+ const entries = computed(
15
+ () => getFormPageSections(root.schema.value).filter(
16
+ (entry) => isFormPageSectionVisible(entry, runtime)
17
+ )
18
+ );
19
+ const requiredEnforced = computed(
20
+ () => root.validationMode.value === true || root.validationMode.value === "required"
21
+ );
22
+ const items = computed(
23
+ () => entries.value.map((entry, index) => ({
24
+ entry,
25
+ section: resolveFormPageSectionState({
26
+ entry,
27
+ index,
28
+ input: root.input.value,
29
+ requiredEnforced: requiredEnforced.value,
30
+ runtime
31
+ })
32
+ }))
33
+ );
34
+ const sections = computed(
35
+ () => items.value.map((item) => item.section)
36
+ );
37
+ const dirtyCheck = computed(() => getSchemaDirtyCheck(root.schema.value));
38
+ const navigationTitle = computed(() => getFormPageNavigationTitle(root.schema.value));
39
+ const remaining = computed(
40
+ () => sections.value.filter((section) => !section.optional && section.status !== "complete").length
41
+ );
42
+ const modified = computed(() => sections.value.filter((section) => section.dirty).length);
43
+ const stickyOffset = ref(0);
44
+ const scroll = useFormPageScroll({ hash: params.hash, root: params.element, sections });
45
+ async function resetSection(key) {
46
+ const section = sections.value.find((candidate) => candidate.key === key);
47
+ if (!section) {
48
+ return;
49
+ }
50
+ for (const path of section.dirtyPaths) {
51
+ runtime.setValue(path, cloneFormValue(runtime.getInitialValue(path)));
52
+ runtime.clearError(path);
53
+ }
54
+ await runtime.settleEffects();
55
+ }
56
+ return {
57
+ active: scroll.active,
58
+ cancel: root.cancel,
59
+ dirtyCheck,
60
+ items,
61
+ modified,
62
+ navigationTitle,
63
+ register: scroll.register,
64
+ remaining,
65
+ resetSection,
66
+ root,
67
+ scrollTo: scroll.scrollTo,
68
+ sections,
69
+ stickyOffset
70
+ };
71
+ }
72
+ const formPageKey = Symbol("nuxt-ui-tools-form-page");
73
+ export function provideFormPage(page) {
74
+ provide(formPageKey, page);
75
+ return page;
76
+ }
77
+ export function useFormPageContext() {
78
+ const page = inject(formPageKey, null);
79
+ if (!page) {
80
+ throw new Error("Form page parts must be rendered inside <FormPage>.");
81
+ }
82
+ return page;
83
+ }
@@ -0,0 +1,34 @@
1
+ import type { FormObject, FormRendererController, FormSubmitHandlerResult, FormUiConfig, FormValidationMode, FormValue } from '../types/index.js';
2
+ export interface UseFormRootParams {
3
+ /** Controller from `useForm`. Its schema, input, and policies win over the direct ones. */
4
+ form: () => FormRendererController | undefined;
5
+ schema: () => FormValue;
6
+ input: () => FormObject | undefined;
7
+ syncInput: () => boolean | readonly string[] | undefined;
8
+ validate: () => FormValidationMode | undefined;
9
+ ui: () => FormUiConfig | undefined;
10
+ onSubmitted: (value: FormObject, result: FormSubmitHandlerResult<FormValue>) => void;
11
+ onCancelled: (value: FormObject) => void;
12
+ }
13
+ /**
14
+ * Root of a rendered form. It creates the runtime and binds it to the controller, provides the
15
+ * runtime and the presentation config to the fields, guards navigation away from a dirty form,
16
+ * and owns submit and cancel. `<NutForm>` and `FormPage` both render from it.
17
+ */
18
+ export declare function useFormRoot(params: UseFormRootParams): {
19
+ cancel: () => void;
20
+ contextError: import("vue").ComputedRef<string | undefined>;
21
+ contextLoading: import("vue").ComputedRef<boolean>;
22
+ contextPending: import("vue").ComputedRef<boolean>;
23
+ formUi: {
24
+ controlSize: import("vue").ComputedRef<import("../types/index.js").FormControlSize>;
25
+ density: import("vue").ComputedRef<import("../types/index.js").FormDensity>;
26
+ ui: import("vue").ComputedRef<FormUiConfig>;
27
+ };
28
+ input: import("vue").ComputedRef<import("../../shared/index.js").GenericObject | undefined>;
29
+ refreshContext: () => Promise<void>;
30
+ runtime: import("../types/index.js").FormRuntime;
31
+ schema: import("vue").ComputedRef<{}>;
32
+ submit: () => Promise<void>;
33
+ validationMode: import("vue").ComputedRef<FormValidationMode>;
34
+ };
@@ -0,0 +1,171 @@
1
+ import { useAppConfig } from "nuxt/app";
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, watch } from "vue";
3
+ import { useRouter } from "vue-router";
4
+ import { useUiToolsLocale } from "../../i18n/use-locale.js";
5
+ import {
6
+ getSchemaAutoFocus,
7
+ getSchemaDirtyNavigation,
8
+ getSchemaSyncInput,
9
+ getSchemaUi,
10
+ getSchemaValidationMode
11
+ } from "../utils/controls.js";
12
+ import { isRecord } from "../utils/path.js";
13
+ import { isString, isUndefined, stringArray } from "../utils/predicate.js";
14
+ import { collectFormFieldsPaths } from "../utils/state.js";
15
+ import { resolveFormText } from "../utils/text.js";
16
+ import { mergeFormUi, resolveAppFormUi } from "../utils/ui.js";
17
+ import { provideFormRuntime, useFormRuntime } from "./use-form-runtime.js";
18
+ import { provideFormUi } from "./use-form-ui.js";
19
+ const DEFAULT_DIRTY_NAVIGATION_MESSAGE = "You have unsaved changes. Close this form?";
20
+ export function useFormRoot(params) {
21
+ const router = useRouter();
22
+ const { t } = useUiToolsLocale();
23
+ const appConfig = useAppConfig();
24
+ const schema = computed(() => params.form()?.schema.value ?? params.schema() ?? {});
25
+ const formUi = provideFormUi(
26
+ computed(
27
+ () => mergeFormUi(resolveAppFormUi(appConfig), getSchemaUi(schema.value), params.ui())
28
+ )
29
+ );
30
+ const input = computed(() => params.form()?.input.value ?? params.input());
31
+ const syncInput = computed(
32
+ () => params.form()?.syncInput.value ?? params.syncInput() ?? getSchemaSyncInput(schema.value)
33
+ );
34
+ const validationMode = computed(
35
+ () => params.form()?.validationMode.value ?? params.validate() ?? getSchemaValidationMode(schema.value)
36
+ );
37
+ const runtime = useFormRuntime({ input, schema, syncInput, validationMode });
38
+ provideFormRuntime(runtime);
39
+ watch(
40
+ params.form,
41
+ (controller, previousController) => {
42
+ previousController?.unbind(runtime);
43
+ controller?.bind(runtime);
44
+ },
45
+ { immediate: true }
46
+ );
47
+ onBeforeUnmount(() => params.form()?.unbind(runtime));
48
+ onMounted(async () => {
49
+ await nextTick();
50
+ const target = getSchemaAutoFocus(schema.value);
51
+ if (isString(target)) {
52
+ await runtime.focusField(target);
53
+ return;
54
+ }
55
+ if (target === true) {
56
+ await focusFirstField();
57
+ }
58
+ });
59
+ if (import.meta.client) {
60
+ let handleBeforeUnload = function(event) {
61
+ if (!shouldConfirmDirtyNavigation()) {
62
+ return;
63
+ }
64
+ event.preventDefault();
65
+ event.returnValue = "";
66
+ };
67
+ window.addEventListener("beforeunload", handleBeforeUnload);
68
+ onBeforeUnmount(() => window.removeEventListener("beforeunload", handleBeforeUnload));
69
+ }
70
+ const removeRouteGuard = router.beforeEach((to, from) => {
71
+ if (runtime.actionPending.value === "submit" || isSameDocument(to, from)) {
72
+ return true;
73
+ }
74
+ if (!shouldConfirmDirtyNavigation()) {
75
+ return true;
76
+ }
77
+ return window.confirm(getDirtyNavigationMessage());
78
+ });
79
+ onBeforeUnmount(removeRouteGuard);
80
+ const contextPending = computed(
81
+ () => Object.values(runtime.context).some(
82
+ (resource) => "pending" in resource && resource.pending && isUndefined(resource.value)
83
+ )
84
+ );
85
+ const contextLoading = computed(
86
+ () => Object.values(runtime.context).some((resource) => "loading" in resource && resource.loading)
87
+ );
88
+ const contextError = computed(() => {
89
+ for (const resource of Object.values(runtime.context)) {
90
+ if (!("error" in resource) || !resource.error) {
91
+ continue;
92
+ }
93
+ if (resource.error instanceof Error) {
94
+ return resource.error.message;
95
+ }
96
+ if (isString(resource.error)) {
97
+ return resource.error;
98
+ }
99
+ return t("form.states.contextError.description");
100
+ }
101
+ return void 0;
102
+ });
103
+ async function refreshContext() {
104
+ await Promise.all(
105
+ Object.values(runtime.context).flatMap(
106
+ (resource) => "refresh" in resource ? [resource.refresh()] : []
107
+ )
108
+ );
109
+ }
110
+ async function submit() {
111
+ const controller = params.form();
112
+ const result = controller ? await controller.submitHandler() : await runtime.submitHandler();
113
+ if (result.success) {
114
+ params.onSubmitted(runtime.output.value, result);
115
+ }
116
+ }
117
+ function cancel() {
118
+ if (shouldConfirmDirtyNavigation() && !window.confirm(getDirtyNavigationMessage())) {
119
+ return;
120
+ }
121
+ params.onCancelled(runtime.output.value);
122
+ }
123
+ function shouldConfirmDirtyNavigation() {
124
+ if (!runtime.isDirty.value) {
125
+ return false;
126
+ }
127
+ const config = getSchemaDirtyNavigation(schema.value);
128
+ if (!config) {
129
+ return false;
130
+ }
131
+ if (!isRecord(config)) {
132
+ return config === true;
133
+ }
134
+ const ignored = stringArray(config.ignorePaths);
135
+ return runtime.dirtyPaths.value.some(
136
+ (path) => !ignored.some((ignoredPath) => path === ignoredPath || path.startsWith(`${ignoredPath}.`))
137
+ );
138
+ }
139
+ function getDirtyNavigationMessage() {
140
+ const config = getSchemaDirtyNavigation(schema.value);
141
+ if (!isRecord(config)) {
142
+ return DEFAULT_DIRTY_NAVIGATION_MESSAGE;
143
+ }
144
+ const message = Object.getOwnPropertyDescriptor(config, "message")?.value;
145
+ return resolveFormText(message) ?? DEFAULT_DIRTY_NAVIGATION_MESSAGE;
146
+ }
147
+ async function focusFirstField() {
148
+ const parentPath = runtime.currentStepRoot.value ? [runtime.currentStepRoot.value] : [];
149
+ for (const path of collectFormFieldsPaths(runtime.currentFields.value, parentPath)) {
150
+ if (await runtime.focusField(path)) {
151
+ return;
152
+ }
153
+ }
154
+ }
155
+ return {
156
+ cancel,
157
+ contextError,
158
+ contextLoading,
159
+ contextPending,
160
+ formUi,
161
+ input,
162
+ refreshContext,
163
+ runtime,
164
+ schema,
165
+ submit,
166
+ validationMode
167
+ };
168
+ }
169
+ function isSameDocument(to, from) {
170
+ return to.fullPath.split("#")[0] === from.fullPath.split("#")[0];
171
+ }
@@ -137,7 +137,7 @@ export function useFormRuntime(params) {
137
137
  }
138
138
  return valid;
139
139
  }
140
- const submit = useFormSubmission({
140
+ const submission = useFormSubmission({
141
141
  beforeNext: () => isStepped.value ? runBeforeNext() : Promise.resolve(true),
142
142
  focusFirstInvalid: focus.focusFirstInvalid,
143
143
  getApi: () => createPublicFormApi(runtime),
@@ -146,6 +146,13 @@ export function useFormRuntime(params) {
146
146
  getSchema: () => params.schema.value,
147
147
  validate
148
148
  });
149
+ async function submitHandler(externalSubmitHandler) {
150
+ const result = await submission.submitHandler(externalSubmitHandler);
151
+ if (result.success) {
152
+ state.rebaseline();
153
+ }
154
+ return result;
155
+ }
149
156
  let hasInitializedInput = !isUndefined(params.input?.value);
150
157
  state.initialize(params.input?.value);
151
158
  let pendingInput;
@@ -221,7 +228,9 @@ export function useFormRuntime(params) {
221
228
  const isLastStep = computed(() => currentStepIndex.value >= steps.value.length - 1);
222
229
  const canGoPrevious = computed(() => isStepped.value && !isFirstStep.value);
223
230
  const canGoNext = computed(() => isStepped.value && !isLastStep.value);
224
- const actionPending = computed(() => navigationActionPending.value ?? submit.actionPending.value);
231
+ const actionPending = computed(
232
+ () => navigationActionPending.value ?? submission.actionPending.value
233
+ );
225
234
  async function runBeforeNext() {
226
235
  const handler = getSchemaLifecycleHandler(params.schema.value, "onBeforeNext");
227
236
  if (!handler) {
@@ -433,10 +442,10 @@ export function useFormRuntime(params) {
433
442
  state: state.state,
434
443
  steps,
435
444
  submit: async () => {
436
- const result = await submit.submitHandler();
445
+ const result = await submitHandler();
437
446
  return result.success;
438
447
  },
439
- submitHandler: submit.submitHandler,
448
+ submitHandler,
440
449
  trackEffect: effects.track,
441
450
  validate,
442
451
  validateCurrentStep
@@ -1,6 +1,6 @@
1
1
  import { computed, shallowRef, toValue } from "vue";
2
- import { isRecord } from "../utils/path.js";
3
- import { isBoolean, isUndefined, stringArray } from "../utils/predicate.js";
2
+ import { getSchemaSyncInput, getSchemaValidationMode } from "../utils/controls.js";
3
+ import { isUndefined } from "../utils/predicate.js";
4
4
  export function useForm(params) {
5
5
  const runtime = shallowRef(null);
6
6
  const schema = computed(() => toValue(params.schema));
@@ -151,23 +151,3 @@ export function useForm(params) {
151
151
  validationMode
152
152
  };
153
153
  }
154
- function getSchemaSyncInput(schema) {
155
- const controls = getSchemaControls(schema);
156
- const value = controls ? Object.getOwnPropertyDescriptor(controls, "syncInput")?.value : void 0;
157
- if (isBoolean(value)) {
158
- return value;
159
- }
160
- return stringArray(value);
161
- }
162
- function getSchemaValidationMode(schema) {
163
- const controls = getSchemaControls(schema);
164
- const value = controls ? Object.getOwnPropertyDescriptor(controls, "validate")?.value : void 0;
165
- return value === false || value === "required" || value === "validators" ? value : true;
166
- }
167
- function getSchemaControls(schema) {
168
- if (!isRecord(schema)) {
169
- return;
170
- }
171
- const controls = Object.getOwnPropertyDescriptor(schema, "controls")?.value;
172
- return isRecord(controls) ? controls : void 0;
173
- }