@anweb/nuxt-ancore 1.15.11 → 1.16.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/README.md +31 -13
- package/dist/module.json +1 -1
- package/dist/runtime/components/An/Dialogs/Dialogs.vue +3 -1
- package/dist/runtime/components/An/Dialogs/Item.vue +10 -1
- package/dist/runtime/components/An/Dropdown.vue +60 -0
- package/dist/runtime/components/An/Dropdown.vue.d.ts +2 -0
- package/dist/runtime/components/An/Tab.js +1 -1
- package/dist/runtime/composables/useAnData.d.ts +4 -4
- package/dist/runtime/composables/useAnData.js +7 -9
- package/dist/runtime/composables/useAnDialogs.d.ts +2 -1
- package/dist/runtime/composables/useAnDialogs.js +4 -2
- package/dist/runtime/composables/useAnForm.d.ts +1 -1
- package/dist/runtime/composables/useAnI18n.d.ts +1 -1
- package/dist/runtime/composables/useAnI18n.js +21 -11
- package/dist/runtime/composables/useAnList.d.ts +10 -7
- package/dist/runtime/composables/useAnList.js +3 -7
- package/dist/runtime/types/dialogs.d.ts +2 -2
- package/dist/runtime/types/global.d.ts +1 -1
- package/dist/runtime/types/infiniteScroll.d.ts +1 -1
- package/dist/runtime/utils/coreApi.js +1 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,33 +4,51 @@
|
|
|
4
4
|
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
5
|
[![Nuxt][nuxt-src]][nuxt-href]
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A set of composables, components and utilities for Nuxt to handle common tasks: data fetching with SSR, paginated lists, form validation, dialogs, i18n and more.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Quick Setup
|
|
13
|
-
|
|
14
|
-
Install the module to your Nuxt application with one command:
|
|
9
|
+
## Setup
|
|
15
10
|
|
|
16
11
|
```bash
|
|
17
12
|
npm i @anweb/nuxt-ancore
|
|
18
13
|
```
|
|
19
14
|
|
|
20
|
-
Then, add it to your nuxt.config.ts:
|
|
21
15
|
```ts
|
|
22
16
|
export default defineNuxtConfig({
|
|
23
|
-
|
|
17
|
+
modules: ['@anweb/nuxt-ancore']
|
|
24
18
|
})
|
|
25
19
|
```
|
|
26
20
|
|
|
21
|
+
## Composables
|
|
22
|
+
|
|
23
|
+
| Name | Description |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `useAnData` | Data fetching with SSR/CSR support, URL params, query and auto-refresh |
|
|
26
|
+
| `useAnList` | Paginated lists with infinite scroll, filters and reverse mode |
|
|
27
|
+
| `useAnForm` | Form state, validation (async-validator), history and diff tracking |
|
|
28
|
+
| `useAnDialogs` | Dynamic dialogs: open, close, closeAll, swipe-to-dismiss |
|
|
29
|
+
| `useAnI18n` | i18next integration with per-component namespaces and language switching |
|
|
30
|
+
|
|
31
|
+
## Components
|
|
32
|
+
|
|
33
|
+
| Name | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `AnTab` | Headless tab with lazy rendering and state preservation via `display: none` |
|
|
36
|
+
| `AnDropdown` | Dropdown with CSS Anchor Positioning, teleport and ARIA |
|
|
37
|
+
|
|
38
|
+
## Utilities
|
|
39
|
+
|
|
40
|
+
| Name | Description |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `toQuery` | Converts an object to a URL query string, filtering out null/undefined |
|
|
43
|
+
| `asyncInit` | SSR-aware init helper: awaits during SSR, fires without await on CSR |
|
|
44
|
+
| `coreApi` | Core API wrapper around `$fetch` with typed responses |
|
|
27
45
|
|
|
28
46
|
<!-- Badges -->
|
|
29
|
-
[npm-version-src]: https://img.shields.io/npm/v/
|
|
30
|
-
[npm-version-href]: https://npmjs.com/package/
|
|
47
|
+
[npm-version-src]: https://img.shields.io/npm/v/@anweb/nuxt-ancore/latest.svg?style=flat&colorA=020420&colorB=00DC82
|
|
48
|
+
[npm-version-href]: https://npmjs.com/package/@anweb/nuxt-ancore
|
|
31
49
|
|
|
32
|
-
[npm-downloads-src]: https://img.shields.io/npm/dm/
|
|
33
|
-
[npm-downloads-href]: https://npm.chart.dev/
|
|
50
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/@anweb/nuxt-ancore.svg?style=flat&colorA=020420&colorB=00DC82
|
|
51
|
+
[npm-downloads-href]: https://npm.chart.dev/@anweb/nuxt-ancore
|
|
34
52
|
|
|
35
53
|
[nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
|
|
36
54
|
[nuxt-href]: https://nuxt.com
|
package/dist/module.json
CHANGED
|
@@ -3,7 +3,7 @@ import { useScrollLock } from "@vueuse/core";
|
|
|
3
3
|
import { useAnDialogs } from "#imports";
|
|
4
4
|
import { AnDialogsItem } from "#components";
|
|
5
5
|
const Dialogs = useAnDialogs();
|
|
6
|
-
const isLocked = useScrollLock(window);
|
|
6
|
+
const isLocked = useScrollLock(import.meta.client ? window : null);
|
|
7
7
|
const onAnimation = () => {
|
|
8
8
|
isLocked.value = !!Dialogs.items.length;
|
|
9
9
|
};
|
|
@@ -11,8 +11,10 @@ const onAnimation = () => {
|
|
|
11
11
|
|
|
12
12
|
<template>
|
|
13
13
|
<transition-group
|
|
14
|
+
v-bind="$attrs"
|
|
14
15
|
name="an-dialogs"
|
|
15
16
|
tag="div"
|
|
17
|
+
aria-live="polite"
|
|
16
18
|
|
|
17
19
|
@before-enter="onAnimation"
|
|
18
20
|
@after-leave="onAnimation"
|
|
@@ -19,6 +19,7 @@ const top = ref(0);
|
|
|
19
19
|
const active = ref(false);
|
|
20
20
|
const target = ref(null);
|
|
21
21
|
let raf = 0;
|
|
22
|
+
let mouseDownOnBackdrop = false;
|
|
22
23
|
const onSwipeStart = () => {
|
|
23
24
|
active.value = canSwipe.value;
|
|
24
25
|
};
|
|
@@ -50,7 +51,9 @@ const canSwipe = computed(() => Scroll.arrivedState.top && !config.value.fullscr
|
|
|
50
51
|
onMounted(() => {
|
|
51
52
|
target.value = refDialog.value?.$el || null;
|
|
52
53
|
if (!config.value.fullscreen) {
|
|
53
|
-
onClickOutside(refDialog, () => Dialogs.close(props.dialog)
|
|
54
|
+
onClickOutside(refDialog, () => Dialogs.close(props.dialog), {
|
|
55
|
+
ignore: [".an-dialog"]
|
|
56
|
+
});
|
|
54
57
|
}
|
|
55
58
|
Swipe = useSwipe(target, {
|
|
56
59
|
onSwipeStart,
|
|
@@ -65,8 +68,14 @@ onMounted(() => {
|
|
|
65
68
|
|
|
66
69
|
<template>
|
|
67
70
|
<div
|
|
71
|
+
v-bind="$attrs"
|
|
72
|
+
role="dialog"
|
|
73
|
+
aria-modal="true"
|
|
68
74
|
class="an-dialog -flex -flex__column"
|
|
69
75
|
:class="[{ '-fullscreen': config.fullscreen }, config.class]"
|
|
76
|
+
@mousedown.self="mouseDownOnBackdrop = true"
|
|
77
|
+
@click.self="mouseDownOnBackdrop && !config.fullscreen && Dialogs.close(props.dialog);
|
|
78
|
+
mouseDownOnBackdrop = false"
|
|
70
79
|
>
|
|
71
80
|
<component
|
|
72
81
|
ref="refDialog"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
import { computed, onMounted, ref, useTemplateRef } from "vue";
|
|
4
|
+
import { onClickOutside, useEventListener } from "@vueuse/core";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
area: { type: String, required: false }
|
|
7
|
+
});
|
|
8
|
+
const id = ref();
|
|
9
|
+
const state = ref(false);
|
|
10
|
+
const refTarget = useTemplateRef("refTarget");
|
|
11
|
+
const toggle = (value) => {
|
|
12
|
+
state.value = value !== void 0 ? value : !state.value;
|
|
13
|
+
};
|
|
14
|
+
const close = () => {
|
|
15
|
+
toggle(false);
|
|
16
|
+
};
|
|
17
|
+
const onKeydown = (e) => {
|
|
18
|
+
if (e.key === "Escape" && state.value) close();
|
|
19
|
+
};
|
|
20
|
+
const area = computed(() => {
|
|
21
|
+
return props.area || "bottom span-right";
|
|
22
|
+
});
|
|
23
|
+
const name = computed(() => {
|
|
24
|
+
return id.value ? `--an-dropdown-${id.value}` : "";
|
|
25
|
+
});
|
|
26
|
+
const menuId = computed(() => id.value ? `an-dropdown-menu-${id.value}` : void 0);
|
|
27
|
+
onMounted(() => {
|
|
28
|
+
id.value = v4();
|
|
29
|
+
});
|
|
30
|
+
onClickOutside(refTarget, close);
|
|
31
|
+
useEventListener(document, "keydown", onKeydown);
|
|
32
|
+
defineExpose({ toggle, close });
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<div ref="refTarget" class="an-dropdown__button" v-bind="$attrs">
|
|
37
|
+
<div
|
|
38
|
+
:aria-expanded="state"
|
|
39
|
+
:aria-controls="menuId"
|
|
40
|
+
aria-haspopup="true"
|
|
41
|
+
>
|
|
42
|
+
<slot name="button" :toggle="toggle" />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<teleport to="body">
|
|
46
|
+
<div
|
|
47
|
+
v-show="state"
|
|
48
|
+
:id="menuId"
|
|
49
|
+
role="menu"
|
|
50
|
+
class="an-dropdown__menu"
|
|
51
|
+
>
|
|
52
|
+
<slot name="menu" :close="close" />
|
|
53
|
+
</div>
|
|
54
|
+
</teleport>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.an-dropdown__button{anchor-name:v-bind(name)}.an-dropdown__menu{position:fixed;position-anchor:v-bind(name);position-area:v-bind(area);position-try-fallbacks:top span-right,bottom span-left,top span-left;z-index:1000}
|
|
60
|
+
</style>
|
|
@@ -12,10 +12,10 @@ interface TUseAnData<TData, TError> {
|
|
|
12
12
|
refresh: () => void;
|
|
13
13
|
config: Ref<TConfig>;
|
|
14
14
|
params: TConfig['params'];
|
|
15
|
-
data:
|
|
16
|
-
status:
|
|
15
|
+
data: Readonly<Ref<TData | undefined>>;
|
|
16
|
+
status: Readonly<Ref<AsyncDataRequestStatus>>;
|
|
17
17
|
loading: ComputedRef<boolean>;
|
|
18
|
-
error:
|
|
18
|
+
error: Readonly<Ref<TError | undefined>>;
|
|
19
19
|
}
|
|
20
|
-
export declare const useAnData: <TData = unknown, TError = unknown>(initConfig: Omit<TConfig, "params"> & Partial<TConfig
|
|
20
|
+
export declare const useAnData: <TData = unknown, TError = unknown>(initConfig: Omit<TConfig, "params"> & Partial<Pick<TConfig, "params">>) => TUseAnData<TData, TError>;
|
|
21
21
|
export {};
|
|
@@ -8,14 +8,15 @@ export const useAnData = (initConfig) => {
|
|
|
8
8
|
const status = ref("idle");
|
|
9
9
|
const isMounted = ref(false);
|
|
10
10
|
const time = ref(0);
|
|
11
|
+
const fetchData = () => userApi(
|
|
12
|
+
path.value.url,
|
|
13
|
+
{ method: "GET", ...config.value.apiConfig || {} }
|
|
14
|
+
);
|
|
11
15
|
const init = async () => {
|
|
12
16
|
if (isMounted.value) {
|
|
13
17
|
const execute = () => {
|
|
14
18
|
status.value = "pending";
|
|
15
|
-
|
|
16
|
-
path.value.url,
|
|
17
|
-
{ method: "GET", ...config.value.apiConfig || {} }
|
|
18
|
-
).then((response) => {
|
|
19
|
+
fetchData().then((response) => {
|
|
19
20
|
data.value = response;
|
|
20
21
|
nextTick().then(() => {
|
|
21
22
|
status.value = "success";
|
|
@@ -25,14 +26,11 @@ export const useAnData = (initConfig) => {
|
|
|
25
26
|
error.value = e;
|
|
26
27
|
});
|
|
27
28
|
};
|
|
28
|
-
watch(() => key.value, execute, { immediate: true
|
|
29
|
+
watch(() => key.value, execute, { immediate: true });
|
|
29
30
|
} else {
|
|
30
31
|
const Data = useAsyncData(
|
|
31
32
|
key,
|
|
32
|
-
() =>
|
|
33
|
-
path.value.url,
|
|
34
|
-
{ method: "GET", ...config.value.apiConfig || {} }
|
|
35
|
-
),
|
|
33
|
+
() => fetchData(),
|
|
36
34
|
{ immediate: false }
|
|
37
35
|
);
|
|
38
36
|
await Data.execute();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type Component } from 'vue';
|
|
1
2
|
import type { TDialog } from '#ancore/types';
|
|
2
3
|
export declare const useAnDialogs: () => {
|
|
3
|
-
open: (component:
|
|
4
|
+
open: (component: Component, params?: Record<string, unknown>, config?: Partial<TDialog>) => TDialog;
|
|
4
5
|
close: (dialog: TDialog) => void;
|
|
5
6
|
closeAll: () => void;
|
|
6
7
|
items: TDialog[];
|
|
@@ -14,8 +14,10 @@ export const useAnDialogs = () => {
|
|
|
14
14
|
return data;
|
|
15
15
|
};
|
|
16
16
|
const close = (dialog) => {
|
|
17
|
-
StateDialogs.value.
|
|
18
|
-
if (
|
|
17
|
+
const index = StateDialogs.value.indexOf(dialog);
|
|
18
|
+
if (index === -1) return;
|
|
19
|
+
StateDialogs.value.splice(index, 1);
|
|
20
|
+
dialog.onClose?.();
|
|
19
21
|
};
|
|
20
22
|
const closeAll = () => {
|
|
21
23
|
while (StateDialogs.value.length) {
|
|
@@ -4,7 +4,7 @@ interface TStructureItem<TData> {
|
|
|
4
4
|
default: TData;
|
|
5
5
|
rules?: RuleItem[];
|
|
6
6
|
}
|
|
7
|
-
export declare const useAnForm: <TForm extends object, TData extends Partial<TForm> | null | (Record<string,
|
|
7
|
+
export declare const useAnForm: <TForm extends object, TData extends Partial<TForm> | null | (Record<string, unknown> & Partial<TForm>) = Partial<TForm> | null>(params: Record<keyof TForm, TStructureItem<TForm[keyof TForm]>>, data?: TData) => {
|
|
8
8
|
state: [TForm] extends [import("vue").Ref<any, any>] ? import("@vue/shared").IfAny<TForm, import("vue").Ref<TForm, TForm>, TForm> : import("vue").Ref<import("vue").UnwrapRef<TForm>, TForm | import("vue").UnwrapRef<TForm>>;
|
|
9
9
|
diff: import("vue").ComputedRef<Partial<TForm>>;
|
|
10
10
|
merge: (data: Partial<TForm>, commit?: true) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const useAnI18n: (resources?: Record<string,
|
|
1
|
+
export declare const useAnI18n: (resources?: Record<string, Record<string, string>>) => {
|
|
2
2
|
lang: string;
|
|
3
3
|
set: (lang: string) => void;
|
|
4
4
|
t: import("i18next").TFunction<["translation", ...string[]], undefined>;
|
|
@@ -4,23 +4,33 @@ export const useAnI18n = (resources) => {
|
|
|
4
4
|
const returnObj = {
|
|
5
5
|
lang: i18next.language,
|
|
6
6
|
set: (lang) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
try {
|
|
8
|
+
i18next.changeLanguage(lang);
|
|
9
|
+
returnObj.lang = lang;
|
|
10
|
+
const config = useRuntimeConfig().public.i18n;
|
|
11
|
+
if (config?.cookie) {
|
|
12
|
+
const cookie = useCookie(config?.cookie, {
|
|
13
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
14
|
+
path: "/",
|
|
15
|
+
sameSite: "lax",
|
|
16
|
+
secure: true
|
|
17
|
+
});
|
|
18
|
+
cookie.value = lang;
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error("[AnI18n] Failed to set language:", e);
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
};
|
|
19
25
|
if (!resources) return { t: i18next.t, ...returnObj };
|
|
20
26
|
const ns = JSON.stringify(resources);
|
|
21
27
|
for (const lang in resources) {
|
|
22
|
-
|
|
23
|
-
i18next.
|
|
28
|
+
try {
|
|
29
|
+
if (!i18next.hasResourceBundle(lang, ns)) {
|
|
30
|
+
i18next.addResourceBundle(lang, ns, resources[lang]);
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(`[AnI18n] Failed to add resource bundle for ${lang}:`, e);
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
const t = (key, options = {}) => i18next.t(key, { ns: [ns, "translation"], ...options });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack';
|
|
2
|
+
import { type Ref, type ComputedRef } from 'vue';
|
|
2
3
|
import type { TInfiniteScroll } from '#ancore/types';
|
|
4
|
+
import { type AsyncDataRequestStatus } from '#app';
|
|
3
5
|
interface TConfig<TFilter> {
|
|
4
6
|
request: NitroFetchRequest;
|
|
5
7
|
apiConfig?: NitroFetchOptions<string>;
|
|
@@ -8,16 +10,17 @@ interface TConfig<TFilter> {
|
|
|
8
10
|
skipField?: string;
|
|
9
11
|
reverse?: boolean;
|
|
10
12
|
}
|
|
11
|
-
|
|
13
|
+
interface TUseAnList<TData, TFilter> {
|
|
12
14
|
init: () => Promise<void>;
|
|
13
15
|
infiniteScroll: (scrollConfig?: TInfiniteScroll) => () => void;
|
|
14
16
|
filter: TFilter;
|
|
15
17
|
params: Record<string, string> | undefined;
|
|
16
18
|
items: TData[];
|
|
17
|
-
count:
|
|
18
|
-
inited:
|
|
19
|
-
status:
|
|
20
|
-
loading:
|
|
21
|
-
error:
|
|
22
|
-
}
|
|
19
|
+
count: Ref<number | null>;
|
|
20
|
+
inited: ComputedRef<boolean>;
|
|
21
|
+
status: Readonly<Ref<AsyncDataRequestStatus>>;
|
|
22
|
+
loading: ComputedRef<boolean>;
|
|
23
|
+
error: Readonly<Ref<unknown | undefined>>;
|
|
24
|
+
}
|
|
25
|
+
export declare const useAnList: <TData, TFilter extends object = {}>(initConfig: TConfig<TFilter>) => TUseAnList<TData, TFilter>;
|
|
23
26
|
export {};
|
|
@@ -12,7 +12,7 @@ export const useAnList = (initConfig) => {
|
|
|
12
12
|
const refresh = () => {
|
|
13
13
|
if (!data.data.value) return;
|
|
14
14
|
if (!config.value.apiConfig?.query?.[config.value.skipField || "skip"]) {
|
|
15
|
-
|
|
15
|
+
count.value = null;
|
|
16
16
|
items.length = 0;
|
|
17
17
|
}
|
|
18
18
|
if (config.value.reverse) {
|
|
@@ -20,10 +20,7 @@ export const useAnList = (initConfig) => {
|
|
|
20
20
|
} else {
|
|
21
21
|
items.push(...data.data.value.items);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
const setCount = (value) => {
|
|
26
|
-
count.value = value;
|
|
23
|
+
count.value = data.data.value.count;
|
|
27
24
|
};
|
|
28
25
|
const infiniteScroll = (scrollConfig) => {
|
|
29
26
|
const onLoadMore = scrollConfig?.onLoadMore || (() => {
|
|
@@ -31,8 +28,7 @@ export const useAnList = (initConfig) => {
|
|
|
31
28
|
config.value.filter[config.value.skipField || "skip"] = items.length;
|
|
32
29
|
});
|
|
33
30
|
const canLoadMore = scrollConfig?.options?.canLoadMore || (() => {
|
|
34
|
-
return (scrollConfig?.canLoadMore?.() ?? true) && inited.value && data.status.value !== "pending" && items.length < (count.value || 0) &&
|
|
35
|
-
!!config.value.filter.limit;
|
|
31
|
+
return (scrollConfig?.canLoadMore?.() ?? true) && inited.value && data.status.value !== "pending" && items.length < (count.value || 0) && !!config.value.filter?.limit;
|
|
36
32
|
});
|
|
37
33
|
const { reset } = useInfiniteScroll(
|
|
38
34
|
scrollConfig?.element || window,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Component } from '
|
|
1
|
+
import type { Component } from 'vue';
|
|
2
2
|
export interface TDialog {
|
|
3
3
|
id: string;
|
|
4
|
-
component: Component
|
|
4
|
+
component: Component;
|
|
5
5
|
params: Record<string, unknown>;
|
|
6
6
|
fullscreen?: boolean;
|
|
7
7
|
class?: string;
|
|
@@ -14,7 +14,7 @@ declare module 'nuxt/schema' {
|
|
|
14
14
|
interface PublicRuntimeConfig {
|
|
15
15
|
i18n?: InitOptions<unknown> & {
|
|
16
16
|
cookie?: string
|
|
17
|
-
resources?: Record<string, { translation:
|
|
17
|
+
resources?: Record<string, { translation: string | Record<string, unknown> }>
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
}
|