@naptics/vue-collection 0.2.15 → 0.3.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/.github/workflows/build.yml +26 -0
- package/.github/workflows/deploy-demo.yml +46 -0
- package/.github/workflows/deploy-lib.yml +59 -0
- package/.gitlab-ci.yml +57 -0
- package/.nvmrc +1 -0
- package/.prettierrc +8 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/launch.json +23 -0
- package/.vscode/settings.json +13 -0
- package/babel.config.json +3 -0
- package/components/NAlert.d.ts +1 -44
- package/components/NBadge.d.ts +1 -133
- package/components/NBreadcrub.d.ts +2 -106
- package/components/NBreadcrub.js +1 -1
- package/components/NButton.d.ts +2 -118
- package/components/NCheckbox.d.ts +1 -32
- package/components/NCheckboxLabel.d.ts +1 -45
- package/components/NCheckboxLabel.js +1 -1
- package/components/NCrudModal.d.ts +7 -251
- package/components/NCrudModal.js +1 -1
- package/components/NDialog.d.ts +1 -110
- package/components/NDialog.js +1 -1
- package/components/NDropdown.d.ts +1 -69
- package/components/NDropdown.js +1 -1
- package/components/NDropzone.d.ts +1 -115
- package/components/NDropzone.js +1 -1
- package/components/NForm.d.ts +1 -23
- package/components/NFormModal.d.ts +7 -151
- package/components/NIconButton.d.ts +3 -159
- package/components/NIconButton.js +1 -1
- package/components/NIconCircle.d.ts +1 -87
- package/components/NInput.d.ts +1 -164
- package/components/NInput.js +1 -1
- package/components/NInputPhone.d.ts +2 -114
- package/components/NInputPhone.js +1 -1
- package/components/NInputSelect.d.ts +2 -187
- package/components/NInputSelect.js +1 -1
- package/components/NInputSuggestion.d.ts +2 -155
- package/components/NInputSuggestion.js +1 -1
- package/components/NLink.d.ts +6 -70
- package/components/NLink.js +8 -1
- package/components/NList.d.ts +1 -43
- package/components/NList.js +1 -1
- package/components/NLoadingIndicator.d.ts +1 -49
- package/components/NModal.d.ts +12 -250
- package/components/NModal.js +15 -9
- package/components/NPagination.d.ts +1 -63
- package/components/NSearchbar.d.ts +1 -56
- package/components/NSearchbarList.d.ts +3 -63
- package/components/NSearchbarList.js +1 -1
- package/components/NSelect.d.ts +2 -148
- package/components/NSelect.js +1 -1
- package/components/NSuggestionList.d.ts +3 -126
- package/components/NSuggestionList.js +5 -2
- package/components/NTable.d.ts +1 -85
- package/components/NTable.js +12 -6
- package/components/NTableAction.d.ts +2 -46
- package/components/NTableAction.js +1 -1
- package/components/NTextArea.d.ts +2 -181
- package/components/NTextArea.js +1 -1
- package/components/NTooltip.d.ts +1 -105
- package/components/NTooltip.js +1 -1
- package/components/NValInput.d.ts +7 -182
- package/components/NValInput.js +1 -1
- package/env.d.ts +15 -0
- package/eslint.config.cjs +29 -0
- package/index.html +13 -0
- package/package.json +21 -19
- package/postcss.config.js +6 -0
- package/public/favicon.ico +0 -0
- package/scripts/build-lib.sh +52 -0
- package/scripts/sync-node-types.js +70 -0
- package/src/demo/App.css +9 -0
- package/src/demo/App.tsx +5 -0
- package/src/demo/components/ColorGrid.tsx +26 -0
- package/src/demo/components/ComponentGrid.tsx +26 -0
- package/src/demo/components/ComponentSection.tsx +30 -0
- package/src/demo/components/VariantSection.tsx +18 -0
- package/src/demo/i18n/de.ts +7 -0
- package/src/demo/i18n/en.ts +7 -0
- package/src/demo/i18n/index.ts +24 -0
- package/src/demo/main.ts +13 -0
- package/src/demo/router/index.ts +21 -0
- package/src/demo/views/HomeView.tsx +94 -0
- package/src/demo/views/NavigationView.tsx +43 -0
- package/src/demo/views/presentation/AlertView.tsx +40 -0
- package/src/demo/views/presentation/BadgeView.tsx +61 -0
- package/src/demo/views/presentation/BreadcrumbView.tsx +52 -0
- package/src/demo/views/presentation/ButtonView.tsx +49 -0
- package/src/demo/views/presentation/CheckboxView.tsx +59 -0
- package/src/demo/views/presentation/DropdownView.tsx +59 -0
- package/src/demo/views/presentation/DropzoneView.tsx +39 -0
- package/src/demo/views/presentation/IconButtonView.tsx +47 -0
- package/src/demo/views/presentation/IconCircleView.tsx +38 -0
- package/src/demo/views/presentation/InputView.tsx +179 -0
- package/src/demo/views/presentation/LinkView.tsx +60 -0
- package/src/demo/views/presentation/ListView.tsx +29 -0
- package/src/demo/views/presentation/LoadingIndicatorView.tsx +38 -0
- package/src/demo/views/presentation/ModalView.tsx +210 -0
- package/src/demo/views/presentation/PaginationView.tsx +25 -0
- package/src/demo/views/presentation/SearchbarView.tsx +80 -0
- package/src/demo/views/presentation/TableView.tsx +146 -0
- package/src/demo/views/presentation/TooltipView.tsx +86 -0
- package/src/lib/components/NAlert.tsx +85 -0
- package/src/lib/components/NBadge.tsx +75 -0
- package/src/lib/components/NBreadcrub.tsx +97 -0
- package/src/lib/components/NButton.tsx +80 -0
- package/src/lib/components/NCheckbox.tsx +55 -0
- package/src/lib/components/NCheckboxLabel.tsx +51 -0
- package/src/lib/components/NCrudModal.tsx +133 -0
- package/src/lib/components/NDialog.tsx +182 -0
- package/src/lib/components/NDropdown.tsx +167 -0
- package/src/lib/components/NDropzone.tsx +265 -0
- package/src/lib/components/NForm.tsx +32 -0
- package/src/lib/components/NFormModal.tsx +66 -0
- package/src/lib/components/NIconButton.tsx +92 -0
- package/src/lib/components/NIconCircle.tsx +78 -0
- package/src/lib/components/NInput.css +11 -0
- package/src/lib/components/NInput.tsx +139 -0
- package/src/lib/components/NInputPhone.tsx +53 -0
- package/src/lib/components/NInputSelect.tsx +126 -0
- package/src/lib/components/NInputSuggestion.tsx +80 -0
- package/src/lib/components/NLink.tsx +82 -0
- package/src/lib/components/NList.tsx +67 -0
- package/src/lib/components/NLoadingIndicator.css +46 -0
- package/src/lib/components/NLoadingIndicator.tsx +63 -0
- package/src/lib/components/NModal.tsx +243 -0
- package/src/lib/components/NPagination.css +15 -0
- package/src/lib/components/NPagination.tsx +131 -0
- package/src/lib/components/NSearchbar.tsx +78 -0
- package/src/lib/components/NSearchbarList.tsx +47 -0
- package/src/lib/components/NSelect.tsx +128 -0
- package/src/lib/components/NSuggestionList.tsx +216 -0
- package/src/lib/components/NTable.css +3 -0
- package/src/lib/components/NTable.tsx +247 -0
- package/src/lib/components/NTableAction.tsx +49 -0
- package/src/lib/components/NTextArea.tsx +159 -0
- package/src/lib/components/NTooltip.css +37 -0
- package/src/lib/components/NTooltip.tsx +250 -0
- package/src/lib/components/NValInput.tsx +163 -0
- package/src/lib/components/ValidatedForm.ts +71 -0
- package/src/lib/components/__tests__/NButton.spec.tsx +26 -0
- package/src/lib/components/__tests__/NCheckbox.spec.tsx +39 -0
- package/src/lib/i18n/de/vue-collection.json +58 -0
- package/src/lib/i18n/en/vue-collection.json +58 -0
- package/src/lib/i18n/index.ts +54 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/jsx.d.ts +13 -0
- package/src/lib/utils/__tests__/identifiable.spec.ts +72 -0
- package/src/lib/utils/__tests__/validation.spec.ts +92 -0
- package/src/lib/utils/breakpoints.ts +47 -0
- package/src/lib/utils/component.tsx +131 -0
- package/src/lib/utils/deferred.ts +28 -0
- package/src/lib/utils/identifiable.ts +87 -0
- package/src/lib/utils/stringMaxLength.ts +25 -0
- package/src/lib/utils/tailwind.ts +41 -0
- package/src/lib/utils/utils.ts +90 -0
- package/src/lib/utils/vModel.ts +260 -0
- package/src/lib/utils/validation.ts +189 -0
- package/src/lib/utils/vue.ts +25 -0
- package/tailwind.config.js +38 -0
- package/tsconfig.config.json +9 -0
- package/tsconfig.demo.json +19 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +18 -0
- package/tsconfig.vitest.json +8 -0
- package/utils/breakpoints.d.ts +1 -1
- package/utils/component.d.ts +3 -7
- package/utils/component.js +5 -2
- package/utils/identifiable.js +5 -1
- package/vite.config.ts +28 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isNullish, type Nullish } from './utils'
|
|
2
|
+
|
|
3
|
+
export type Identifiable<Id = string> = Readonly<{ id: Id }>
|
|
4
|
+
export type MaybeIdentifiable<Id = string> = Readonly<{ id?: Nullish<Id> }>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Looks for the `id` in the given array of `Identifiables`.
|
|
8
|
+
* @param array The array to search the item in.
|
|
9
|
+
* @param id The `id` of the desired item.
|
|
10
|
+
* @returns The first item with the specified `id` or `undefined` if none exists.
|
|
11
|
+
*/
|
|
12
|
+
function find<Id, Item extends Identifiable<Id>>(array: Nullish<Item[]>, id: Id): Item | undefined {
|
|
13
|
+
if (isNullish(array)) return undefined
|
|
14
|
+
const filtered = array.filter(item => item.id === id)
|
|
15
|
+
if (filtered.length > 0) return filtered[0]
|
|
16
|
+
else return undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if the given array contains an item with the `id`.
|
|
21
|
+
* @param array The array to search the item in.
|
|
22
|
+
* @param id The `id` to check the array for.
|
|
23
|
+
* @returns `true` if there is at least one item in the array with the given `id`.
|
|
24
|
+
*/
|
|
25
|
+
function contains<Id, Item extends Identifiable<Id>>(array: Item[], id: Id): boolean {
|
|
26
|
+
return find(array, id) !== undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function insertSingle<Id, Item extends Identifiable<Id>>(baseArray: Item[], insertItem: Item) {
|
|
30
|
+
const index = baseArray.findIndex(item => item.id === insertItem.id)
|
|
31
|
+
if (index === -1) {
|
|
32
|
+
baseArray.push(insertItem)
|
|
33
|
+
} else {
|
|
34
|
+
baseArray.splice(index, 1, insertItem)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Inserts the items into the given array, replacing items with the same `id`.
|
|
40
|
+
* If no item with the same `id` exists, the item is appended to the array.
|
|
41
|
+
* If multiple items with the same `id` exist, just the first item is replaced.
|
|
42
|
+
* @param array The array to insert the items into.
|
|
43
|
+
* @param insertItems The items to insert into the array.
|
|
44
|
+
* @returns The reference to the same array, which was passed.
|
|
45
|
+
*/
|
|
46
|
+
function insert<Id, Item extends Identifiable<Id>>(array: Item[], ...insertItems: Item[]): Item[] {
|
|
47
|
+
insertItems.forEach(item => insertSingle(array, item))
|
|
48
|
+
return array
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Removes all items with the specified `ids` from the given array.
|
|
53
|
+
* @param array The array to remove the items from.
|
|
54
|
+
* @param ids The `ids` of the items which should be removed.
|
|
55
|
+
* @returns The reference to the same array, which was passed.
|
|
56
|
+
*/
|
|
57
|
+
function remove<Id, Item extends Identifiable>(array: Item[], ...ids: Id[]): Item[] {
|
|
58
|
+
ids.forEach(id => {
|
|
59
|
+
let noMatches = false
|
|
60
|
+
while (!noMatches) {
|
|
61
|
+
const index = array.findIndex(item => item.id === id)
|
|
62
|
+
if (index != -1) array.splice(index, 1)
|
|
63
|
+
else noMatches = true
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
return array
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compares the two arrays and checks if they both have
|
|
71
|
+
* items with the same `ids` in the same order.
|
|
72
|
+
* @param first The first array to compare.
|
|
73
|
+
* @param second The second array to compare.
|
|
74
|
+
* @returns `true` if the arrays contain item with the same `ids` in the same order.
|
|
75
|
+
*/
|
|
76
|
+
function areSameArrays<Id>(first: Identifiable<Id>[], second: Identifiable<Id>[]): boolean {
|
|
77
|
+
if (first.length != second.length) return false
|
|
78
|
+
for (let i = 0; i < first.length; i++) {
|
|
79
|
+
if (first[i]?.id !== second[i]?.id) return false
|
|
80
|
+
}
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* This object contains utility functions to deal with {@link Identifiable} objects.
|
|
86
|
+
*/
|
|
87
|
+
export const Id = { find, contains, insert, remove, areSameArrays } as const
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a shortened string with '...' at the end if it is longer than the given `maxLength`.
|
|
3
|
+
* @param input the string to shorten
|
|
4
|
+
* @param maxLength the max length
|
|
5
|
+
* @see maxLengthSplitCenter
|
|
6
|
+
*/
|
|
7
|
+
export function maxLength(input: string | null, maxLength: number): string {
|
|
8
|
+
if (input && input.length > maxLength) return `${input.substring(0, maxLength - 3).trim()}...`
|
|
9
|
+
else return input || ''
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns a shortened string with '...' in the center of the string if it is longer than the given `maxLength`.
|
|
14
|
+
* @param input the string to shorten
|
|
15
|
+
* @param maxLength the max length
|
|
16
|
+
* @see maxLength
|
|
17
|
+
*/
|
|
18
|
+
export function maxLengthSplitCenter(input: string | null, maxLength: number): string {
|
|
19
|
+
if (input && input.length > maxLength) {
|
|
20
|
+
const chars = maxLength - 3
|
|
21
|
+
const charsAtStart = Math.ceil(chars / 2)
|
|
22
|
+
const charsAtEnd = Math.floor(chars / 2)
|
|
23
|
+
return `${input.substring(0, charsAtStart).trim()}...${input.substring(input.length - charsAtEnd).trim()}`
|
|
24
|
+
} else return input || ''
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FunctionalComponent, HTMLAttributes, VNodeProps } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type TWTextSize =
|
|
4
|
+
| 'text-xs'
|
|
5
|
+
| 'text-sm'
|
|
6
|
+
| 'text-base'
|
|
7
|
+
| 'text-lg'
|
|
8
|
+
| 'text-xl'
|
|
9
|
+
| 'text-2xl'
|
|
10
|
+
| 'text-3xl'
|
|
11
|
+
| 'text-4xl'
|
|
12
|
+
| 'text-5xl'
|
|
13
|
+
| 'text-6xl'
|
|
14
|
+
| 'text-7xl'
|
|
15
|
+
| 'text-8xl'
|
|
16
|
+
| 'text-9xl'
|
|
17
|
+
|
|
18
|
+
export type TWMaxWidth =
|
|
19
|
+
| 'max-w-xs'
|
|
20
|
+
| 'max-w-sm'
|
|
21
|
+
| 'max-w-md'
|
|
22
|
+
| 'max-w-lg'
|
|
23
|
+
| 'max-w-xl'
|
|
24
|
+
| 'max-w-2xl'
|
|
25
|
+
| 'max-w-3xl'
|
|
26
|
+
| 'max-w-4xl'
|
|
27
|
+
| 'max-w-5xl'
|
|
28
|
+
| 'max-w-6xl'
|
|
29
|
+
| 'max-w-7xl'
|
|
30
|
+
| 'max-w-full'
|
|
31
|
+
| 'max-w-min'
|
|
32
|
+
| 'max-w-max'
|
|
33
|
+
| 'max-w-fit'
|
|
34
|
+
| 'max-w-prose'
|
|
35
|
+
| 'max-w-screen-sm'
|
|
36
|
+
| 'max-w-screen-md'
|
|
37
|
+
| 'max-w-screen-lg'
|
|
38
|
+
| 'max-w-screen-xl'
|
|
39
|
+
| 'max-w-screen-2xl'
|
|
40
|
+
|
|
41
|
+
export type HeroIcon = FunctionalComponent<HTMLAttributes & VNodeProps>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ---------- Null and Undefined ----------
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Ref } from 'vue'
|
|
6
|
+
|
|
7
|
+
export type Optional<T> = T | undefined
|
|
8
|
+
export type Nullable<T> = T | null
|
|
9
|
+
export type Nullish<T> = T | null | undefined
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Determines if a value is not null or undefined.
|
|
13
|
+
* @param value the value to check
|
|
14
|
+
* @returns `true` if the value is anything but `null` or `undefined`.
|
|
15
|
+
*/
|
|
16
|
+
export function notNullish<T>(value: Nullish<T>): value is T {
|
|
17
|
+
return value !== null && value !== undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Determines if a value is null or undefined.
|
|
22
|
+
* @param value the value to check
|
|
23
|
+
* @returns `true` if the value is `null` or `undefined`.
|
|
24
|
+
*/
|
|
25
|
+
export function isNullish(value: Nullish<unknown>): value is null | undefined {
|
|
26
|
+
return value === null || value === undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determines if the value of a ref is not nullish.
|
|
31
|
+
* @param ref the ref to check
|
|
32
|
+
* @returns `true` if the value of the ref is not nullish.
|
|
33
|
+
* @see notNullish
|
|
34
|
+
*/
|
|
35
|
+
export function notNullishRef<T>(ref: Ref<T>): ref is Ref<NonNullable<T>> {
|
|
36
|
+
return notNullish(ref.value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Determines if the value of a ref is null or undefined.
|
|
41
|
+
* @param ref the ref to check
|
|
42
|
+
* @returns `true` if the value of the ref is `null` or `undefined`.
|
|
43
|
+
* @see isNullish
|
|
44
|
+
*/
|
|
45
|
+
export function isNullishRef(ref: Ref<Nullish<unknown>>): ref is Ref<null | undefined> {
|
|
46
|
+
return isNullish(ref.value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* ---------- Objects ----------
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
export type AnyObject = Record<string | number | symbol, unknown>
|
|
54
|
+
export type EmptyObject = Record<string | number | symbol, never>
|
|
55
|
+
|
|
56
|
+
export function isAnyObject(object: unknown): object is AnyObject {
|
|
57
|
+
return typeof object === 'object' && !Array.isArray(object)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Marks all properties of an object (including sub-objects) as readonly.
|
|
62
|
+
*/
|
|
63
|
+
export type ReadonlyObject<T extends AnyObject> = {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
65
|
+
readonly [P in keyof T]: T[P] extends ReadonlyObject<infer _U>
|
|
66
|
+
? T[P]
|
|
67
|
+
: T[P] extends AnyObject
|
|
68
|
+
? ReadonlyObject<T[P]>
|
|
69
|
+
: T[P]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the same object casted to a {@link ReadonlyObject}.
|
|
74
|
+
*/
|
|
75
|
+
export function readonlyObject<T extends AnyObject>(object: T): ReadonlyObject<T> {
|
|
76
|
+
return object as ReadonlyObject<T>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* ---------- Unique Id ----------
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
let currentId = 1
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generates and returns a non random but unique id.
|
|
87
|
+
*/
|
|
88
|
+
export function uniqueId(): number {
|
|
89
|
+
return currentId++
|
|
90
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { PropType, Ref } from 'vue'
|
|
2
|
+
import type { AnyObject } from './utils'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* ---------- VModel as Props ----------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates props for a `v-model` of the given type.
|
|
10
|
+
* A `v-model` consits of a value-property and a update-function.
|
|
11
|
+
* @param propType The propType of the `v-model`.
|
|
12
|
+
* @returns An object containing the value-property and the update-function.
|
|
13
|
+
*/
|
|
14
|
+
export function vModelProps<T>(propType: PropType<T>) {
|
|
15
|
+
return {
|
|
16
|
+
/**
|
|
17
|
+
* The value of the component.
|
|
18
|
+
*/
|
|
19
|
+
value: propType as PropType<T>,
|
|
20
|
+
/**
|
|
21
|
+
* This will be called, when the value of the component has changed.
|
|
22
|
+
*/
|
|
23
|
+
onUpdateValue: Function as PropType<(newValue: T) => void>,
|
|
24
|
+
} as const
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates props for a required `v-model` of the given type.
|
|
29
|
+
* @see {@link vModelProps}
|
|
30
|
+
*/
|
|
31
|
+
export function vModelRequiredProps<T>(propType: PropType<T>) {
|
|
32
|
+
return {
|
|
33
|
+
/**
|
|
34
|
+
* The value of the component.
|
|
35
|
+
*/
|
|
36
|
+
value: {
|
|
37
|
+
type: propType as PropType<T>,
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* This will be called, when the value of the component has changed.
|
|
42
|
+
*/
|
|
43
|
+
onUpdateValue: Function as PropType<(newValue: T) => void>,
|
|
44
|
+
} as const
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates props for a `v-model` of the given type with a default value.
|
|
49
|
+
* @see {@link vModelProps}
|
|
50
|
+
*/
|
|
51
|
+
export function vModelDefaultProps<T>(propType: PropType<T>, defaultValue: () => T) {
|
|
52
|
+
return {
|
|
53
|
+
/**
|
|
54
|
+
* The value of the component.
|
|
55
|
+
*/
|
|
56
|
+
value: {
|
|
57
|
+
type: propType as PropType<T>,
|
|
58
|
+
default: defaultValue,
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* This will be called, when the value of the component has changed.
|
|
62
|
+
*/
|
|
63
|
+
onUpdateValue: Function as PropType<(newValue: T) => void>,
|
|
64
|
+
} as const
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* ---------- VModel to pass to components ----------
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The `VModel` type contains a value and an optional update-function.
|
|
73
|
+
* This object is passed to a component to create a two-way binding.
|
|
74
|
+
*/
|
|
75
|
+
export type VModel<T, U = T> = {
|
|
76
|
+
value: T
|
|
77
|
+
onUpdateValue?: (newValue: U) => void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The `VModelForArray` contains the normal `VModel` properties,
|
|
82
|
+
* but aditionally a function, which removes the value from the array.
|
|
83
|
+
*/
|
|
84
|
+
export type VModelForArray<T, U = T> = VModel<T, U> & {
|
|
85
|
+
remove(): void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Uses the given `ref` as a `v-model`, to create a two-way binding with the `ref`.
|
|
90
|
+
* @param ref The `ref` which should be used as the `v-model`.
|
|
91
|
+
* @returns An object of type {@link VModel}, which connects the `ref` to the `v-model`.
|
|
92
|
+
*/
|
|
93
|
+
export function vModelForRef<T>(ref: Ref<T>): VModel<T> {
|
|
94
|
+
return {
|
|
95
|
+
value: ref.value,
|
|
96
|
+
onUpdateValue: (newValue: T) => {
|
|
97
|
+
ref.value = newValue
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* This creates a `v-model` for a property of an object. The property of the object is taken as the
|
|
104
|
+
* value of the `v-model`, the `onUpdate` function, is called every time when the property is updated
|
|
105
|
+
* with the whole object and its updated property.
|
|
106
|
+
* @param object The object which contains the relevant property.
|
|
107
|
+
* @param key The key of the property which should be used as the v-model.
|
|
108
|
+
* @param onUpdate The updater function which is called with the entire object when the property has changed.
|
|
109
|
+
* @returns An object containing of type {@link VModel}.
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* // inside setup function
|
|
113
|
+
* const customer = ref({ firstName: 'Hansi', lastName: 'Halunk' })
|
|
114
|
+
*
|
|
115
|
+
* // This input needs a v-model for the `firstName` property.
|
|
116
|
+
* return () => (
|
|
117
|
+
* <NInput
|
|
118
|
+
* name="First Name"
|
|
119
|
+
* {...vModelForObjectProperty(customer.value, 'firstName', newVal => (customer.value = newValue))}
|
|
120
|
+
* />
|
|
121
|
+
* )
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function vModelForObjectProperty<T extends AnyObject, K extends keyof T>(
|
|
125
|
+
object: T,
|
|
126
|
+
key: K,
|
|
127
|
+
onUpdate?: (newValue: T) => void
|
|
128
|
+
): VModel<T[K]> {
|
|
129
|
+
return {
|
|
130
|
+
value: object[key],
|
|
131
|
+
onUpdateValue: (newValue: T[K]) => {
|
|
132
|
+
onUpdate?.({ ...object, [key]: newValue })
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* This creates a `v-model` which operates on one property of a parent `v-model`. It takes the value of
|
|
139
|
+
* the property as the value of the `v-model` and calls the function `onUpdateValue` of the parent `v-model`
|
|
140
|
+
* whenever the property changes. This function can be seen as a kind of mapper for a `v-model`.
|
|
141
|
+
* @param vModel The parent `v-model`, which this function extracts a single property from.
|
|
142
|
+
* @param key The key of the relevant property.
|
|
143
|
+
* @returns An object of type {@link VModel}.
|
|
144
|
+
* @example
|
|
145
|
+
* ```tsx
|
|
146
|
+
* type Customer = { firstName: string, lastName: String }
|
|
147
|
+
*
|
|
148
|
+
* // inside setup function,
|
|
149
|
+
* // This vModel would normally be inside the props of a component e.g., `CustomerEditor`.
|
|
150
|
+
* const parentVModel = {
|
|
151
|
+
* value: { firstName: 'Hansi', lastName: 'Halunk' },
|
|
152
|
+
* onUpdateValue: (newValue: Customer) => {
|
|
153
|
+
* // update something
|
|
154
|
+
* }
|
|
155
|
+
* }
|
|
156
|
+
*
|
|
157
|
+
* // This input needs a v-model for the `firstName` property.
|
|
158
|
+
* return () => (
|
|
159
|
+
* <NInput
|
|
160
|
+
* name="First Name"
|
|
161
|
+
* {...vModelForVModelProperty(parentVModel, 'firstName'))}
|
|
162
|
+
* />
|
|
163
|
+
* )
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export function vModelForVModelProperty<
|
|
167
|
+
Model extends VModel<ModelValue>,
|
|
168
|
+
Key extends keyof ModelValue,
|
|
169
|
+
ModelValue extends AnyObject = Model['value'],
|
|
170
|
+
>(vModel: Model, key: Key): VModel<ModelValue[Key]> {
|
|
171
|
+
return {
|
|
172
|
+
value: vModel.value[key],
|
|
173
|
+
onUpdateValue: (newValue: ModelValue[Key]) => {
|
|
174
|
+
vModel.onUpdateValue?.({ ...vModel.value, [key]: newValue })
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* This function does the same thing as {@link vModelForVModelProperty},
|
|
181
|
+
* but can be provided with a mapper function for the modified property.
|
|
182
|
+
* @see {@link vModelForVModelProperty}
|
|
183
|
+
* @example
|
|
184
|
+
* ```tsx
|
|
185
|
+
* type Customer = { firstName: string, lastName: String, type: 'admin' | 'user' }
|
|
186
|
+
*
|
|
187
|
+
* // inside setup function,
|
|
188
|
+
* // This vModel would normally be inside the props of a component e.g., `CustomerEditor`.
|
|
189
|
+
* const parentVModel = {
|
|
190
|
+
* value: { firstName: 'Hansi', lastName: 'Halunk', type: 'user' },
|
|
191
|
+
* onUpdateValue: (newValue: Customer) => {
|
|
192
|
+
* // update something
|
|
193
|
+
* }
|
|
194
|
+
* }
|
|
195
|
+
*
|
|
196
|
+
* // This input needs a v-model for the `type` property.
|
|
197
|
+
* // Unfortunately the NSelect input expects an `onUpdateValue` with a
|
|
198
|
+
* // parameter of type `string`, not type `admin | user`.
|
|
199
|
+
* // So we have to provide a mapper function from `string` to `admin | user`.
|
|
200
|
+
* // In this example, we just cast the value.
|
|
201
|
+
* return () => (
|
|
202
|
+
* <NSelect
|
|
203
|
+
* name="Type"
|
|
204
|
+
* {...vModelForVModelPropertyMapType(parentVModel, 'type', newVal => newVal as 'admin' | 'user'))}
|
|
205
|
+
* />
|
|
206
|
+
* )
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export function vModelForVModelPropertyMapType<
|
|
210
|
+
Model extends VModel<ModelValue>,
|
|
211
|
+
Key extends keyof ModelValue,
|
|
212
|
+
UpdateValue,
|
|
213
|
+
ModelValue extends AnyObject = Model['value'],
|
|
214
|
+
>(vModel: Model, key: Key, mapType: (value: UpdateValue) => ModelValue[Key]): VModel<ModelValue[Key], UpdateValue> {
|
|
215
|
+
return {
|
|
216
|
+
value: vModel.value[key],
|
|
217
|
+
onUpdateValue: (newValue: UpdateValue) => {
|
|
218
|
+
vModel.onUpdateValue?.({ ...vModel.value, [key]: mapType(newValue) })
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Creates an array of `v-models`, one for every element of an array. All changes in
|
|
225
|
+
* a `v-model` of any element, will call the `onUpdate` function, with the updated array.
|
|
226
|
+
* @param array The array to create `v-models` for.
|
|
227
|
+
* @param onUpdate The updater function, which is called whenever an element has changed.
|
|
228
|
+
* @returns An object of type {@link VModelForArray}.
|
|
229
|
+
* @example
|
|
230
|
+
* ```tsx
|
|
231
|
+
* // inside setup function
|
|
232
|
+
* const todos = ref([
|
|
233
|
+
* 'Create v-model helper functions.',
|
|
234
|
+
* 'Document them!'
|
|
235
|
+
* ])
|
|
236
|
+
*
|
|
237
|
+
* // For every todo, there should be an input to modifiy it.
|
|
238
|
+
* return () => (
|
|
239
|
+
* <div>
|
|
240
|
+
* {vModelsForArray(todos.value, newValue => (todos.value = newValue)).map(todoVModel => (
|
|
241
|
+
* <NInput name="Todo" {...todoVModel} />
|
|
242
|
+
* ))}
|
|
243
|
+
* </div>
|
|
244
|
+
* )
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export function vModelsForArray<T>(array: T[], onUpdate?: (newValue: T[]) => void): VModelForArray<T>[] {
|
|
248
|
+
return array.map((entry, index) => ({
|
|
249
|
+
value: entry,
|
|
250
|
+
onUpdateValue: (entry: T) => {
|
|
251
|
+
const newArray = [...array]
|
|
252
|
+
newArray[index] = entry
|
|
253
|
+
onUpdate?.(newArray)
|
|
254
|
+
},
|
|
255
|
+
remove: () => {
|
|
256
|
+
const newArray = [...array.slice(0, index), ...array.slice(index + 1)]
|
|
257
|
+
onUpdate?.(newArray)
|
|
258
|
+
},
|
|
259
|
+
}))
|
|
260
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { trsl } from '../i18n'
|
|
2
|
+
|
|
3
|
+
export type ValidationResultValid = {
|
|
4
|
+
isValid: true
|
|
5
|
+
errorMessage?: undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ValidationResultInvalid = {
|
|
9
|
+
isValid: false
|
|
10
|
+
errorMessage: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type InputValue = string | null | undefined
|
|
14
|
+
export type ValidationResult = ValidationResultValid | ValidationResultInvalid
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `ValidationRule` checks an input for a criteria and returns either
|
|
18
|
+
* a {@link ValidationResultValid} or a {@link ValidationResultInvalid}.
|
|
19
|
+
* A falsy input-value should always return a valid result to not interfere
|
|
20
|
+
* with the {@link required} rule.
|
|
21
|
+
*/
|
|
22
|
+
export type ValidationRule = (input: InputValue) => ValidationResult
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a valid result.
|
|
26
|
+
*/
|
|
27
|
+
export function validResult(): ValidationResultValid {
|
|
28
|
+
return { isValid: true }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates an invalid result with the provided error message.
|
|
33
|
+
*/
|
|
34
|
+
export function invalidResult(errorMessage: string): ValidationResultInvalid {
|
|
35
|
+
return { isValid: false, errorMessage }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TRANSLATION_KEY_BASE = 'vue-collection.validation.rules'
|
|
39
|
+
function invalidResultInternal(key: string, params?: Record<string, unknown>): ValidationResultInvalid {
|
|
40
|
+
return invalidResult(trsl(`${TRANSLATION_KEY_BASE}.${key}`, params))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates a given input with the specified rules.
|
|
45
|
+
* The rules are evaluated in the order they're in the array.
|
|
46
|
+
* The {@link ValidationResult} will either contain the errorMessage of the failed rule
|
|
47
|
+
* or a valid result if all rules passed.
|
|
48
|
+
* @param input the input to validate.
|
|
49
|
+
* @param rules the rules which should be vaildated, in the order of priority.
|
|
50
|
+
* @returns an object containing the result of the validation.
|
|
51
|
+
*/
|
|
52
|
+
export function validate(input: InputValue, rules: ValidationRule[]): ValidationResult {
|
|
53
|
+
for (const rule of rules) {
|
|
54
|
+
const validationResult = rule(input)
|
|
55
|
+
if (!validationResult.isValid) return validationResult
|
|
56
|
+
}
|
|
57
|
+
return validResult()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* ---------- Validation Rules ----------
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This rule expects the trimmed input-value to be truthy.
|
|
66
|
+
*/
|
|
67
|
+
export const required: ValidationRule = input => {
|
|
68
|
+
const trimmed = input?.trim()
|
|
69
|
+
if (trimmed) return validResult()
|
|
70
|
+
else return invalidResultInternal('required')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* This rule expects the input to be an integer.
|
|
75
|
+
*/
|
|
76
|
+
export const integer: ValidationRule = input => {
|
|
77
|
+
if (!input || Number.isInteger(+input)) return validResult()
|
|
78
|
+
else return invalidResultInternal('integer')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* This rule expects the input to be in the specified length range. An empty input
|
|
83
|
+
* will always be allowed by this rule to not interefere with the {@link required} rule.
|
|
84
|
+
* @param min The minimum length of the string.
|
|
85
|
+
* @param max The maximum length of the string.
|
|
86
|
+
*/
|
|
87
|
+
export function length(min: number | undefined, max: number | undefined): ValidationRule {
|
|
88
|
+
return input => {
|
|
89
|
+
if (!input) return validResult()
|
|
90
|
+
|
|
91
|
+
if (min !== undefined && max !== undefined && !(min <= input.length && input.length <= max))
|
|
92
|
+
return invalidResultInternal('length.min-max', { min, max })
|
|
93
|
+
else if (min !== undefined && !(min <= input.length)) return invalidResultInternal('length.min', { min })
|
|
94
|
+
else if (max !== undefined && !(input.length <= max)) return invalidResultInternal('length.max', { max })
|
|
95
|
+
|
|
96
|
+
return validResult()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* This rule expects the input to be a number in the specified range.
|
|
102
|
+
* @param min the lower bound, if `undefined` there is no lower bound.
|
|
103
|
+
* @param max the upper bound, if `undefined` there is no upper bound.
|
|
104
|
+
*/
|
|
105
|
+
export function numberRange(min: number | undefined, max: number | undefined): ValidationRule {
|
|
106
|
+
return input => {
|
|
107
|
+
if (!input) return validResult()
|
|
108
|
+
|
|
109
|
+
const parsed = Number.parseFloat(input)
|
|
110
|
+
if (Number.isNaN(parsed)) return invalidResultInternal('number-range.nan')
|
|
111
|
+
|
|
112
|
+
if (min !== undefined && max !== undefined && !(min <= parsed && parsed <= max))
|
|
113
|
+
return invalidResultInternal('number-range.min-max', { min, max })
|
|
114
|
+
else if (min !== undefined && !(min <= parsed)) return invalidResultInternal('number-range.min', { min })
|
|
115
|
+
else if (max !== undefined && !(parsed <= max)) return invalidResultInternal('number-range.max', { max })
|
|
116
|
+
|
|
117
|
+
return validResult()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const VALIDATION_FORMAT_EMAIL = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* This rule expects the input-value to be a valid email adress matching {@link VALIDATION_FORMAT_EMAIL}.
|
|
125
|
+
*/
|
|
126
|
+
export const email: ValidationRule = input => {
|
|
127
|
+
if (!input || VALIDATION_FORMAT_EMAIL.test(input)) return validResult()
|
|
128
|
+
else return invalidResultInternal('email')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const VALIDATION_FORMAT_PASSWORD = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$^+\-=!*()@%&?]).{8,}$/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* This rule expects the input-value to be a password matching {@link VALIDATION_FORMAT_PASSWORD}.
|
|
135
|
+
*/
|
|
136
|
+
export const password: ValidationRule = input => {
|
|
137
|
+
if (!input || VALIDATION_FORMAT_PASSWORD.test(input)) return validResult()
|
|
138
|
+
else if (input.length < 8) return invalidResultInternal('password.to-short')
|
|
139
|
+
else if (!/[a-z]+/.test(input)) return invalidResultInternal('password.no-lowercase')
|
|
140
|
+
else if (!/[A-Z]+/.test(input)) return invalidResultInternal('password.no-uppercase')
|
|
141
|
+
else if (!/\d+/.test(input)) return invalidResultInternal('password.no-digits')
|
|
142
|
+
else if (!/[#$^+\-=!*()@%&?]+/.test(input)) return invalidResultInternal('password.no-special-chars')
|
|
143
|
+
else return invalidResultInternal('password.unknown')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* This rule expects the input-value to match another (input-) value.
|
|
148
|
+
* In difference to most other rules, this rule does not always return a valid result,
|
|
149
|
+
* when the input is falsy. The input is always required to match the other value.
|
|
150
|
+
* @param other the other value to match
|
|
151
|
+
*/
|
|
152
|
+
export function matches(other: string | null | undefined): ValidationRule {
|
|
153
|
+
return input => {
|
|
154
|
+
if (input === other) return validResult()
|
|
155
|
+
else return invalidResultInternal('matches')
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* This rule expects the input-value to match one option in an array.
|
|
161
|
+
* @param options the options which the input can match
|
|
162
|
+
*/
|
|
163
|
+
export function option(options: string[]): ValidationRule {
|
|
164
|
+
return input => {
|
|
165
|
+
if (!input || options.includes(input || '')) return validResult()
|
|
166
|
+
else return invalidResultInternal('option')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* This rule expects the input-value to match the regex pattern
|
|
172
|
+
* @param pattern the pattern the input should match.
|
|
173
|
+
*/
|
|
174
|
+
export function regex(pattern: RegExp): ValidationRule {
|
|
175
|
+
return input => {
|
|
176
|
+
if (!input || pattern.test(input || '')) return validResult()
|
|
177
|
+
else return invalidResultInternal('regex')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* This rule can be used if the validation logic happens somewhere else.
|
|
183
|
+
* When `isValid = true` is passed, the function will return a valid result,
|
|
184
|
+
* otherwise it will return the invalid result with the passed `errorKey`.
|
|
185
|
+
* Like always, a falsy input is always valid to not interefere with the {@link required} rule.
|
|
186
|
+
*/
|
|
187
|
+
export function external(isValid: boolean, errorMessage: string): ValidationRule {
|
|
188
|
+
return input => (!input || isValid ? validResult() : invalidResult(errorMessage))
|
|
189
|
+
}
|