@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,26 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import NButton from '../NButton'
|
|
4
|
+
|
|
5
|
+
describe('<NButton>', () => {
|
|
6
|
+
it('shows text', () => {
|
|
7
|
+
const button = mount(() => <NButton>Click me!</NButton>)
|
|
8
|
+
expect(button.text()).toMatch('Click me!')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('calls onClick when clicked', async () => {
|
|
12
|
+
const onClick = vi.fn()
|
|
13
|
+
const button = mount(() => <NButton onClick={onClick} />).get('button')
|
|
14
|
+
await button.trigger('click')
|
|
15
|
+
expect(onClick).toHaveBeenCalledOnce()
|
|
16
|
+
await button.trigger('click')
|
|
17
|
+
expect(onClick).toHaveBeenCalledTimes(2)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('does not call onClick when disabled', async () => {
|
|
21
|
+
const onClick = vi.fn()
|
|
22
|
+
const button = mount(() => <NButton onClick={onClick} disabled={true} />).get('button')
|
|
23
|
+
await button.trigger('click')
|
|
24
|
+
expect(onClick).not.toHaveBeenCalled()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
3
|
+
import NCheckbox from '../NCheckbox'
|
|
4
|
+
|
|
5
|
+
describe('<NCheckbox>', () => {
|
|
6
|
+
test('handles value and calls onUpdateValue correctly', async () => {
|
|
7
|
+
const onUpdateValue = vi.fn()
|
|
8
|
+
|
|
9
|
+
let checkbox = mount(() => <NCheckbox value={false} onUpdateValue={onUpdateValue} />).get('input')
|
|
10
|
+
await checkbox.trigger('click')
|
|
11
|
+
expect(onUpdateValue).toHaveBeenLastCalledWith(true)
|
|
12
|
+
expect(onUpdateValue).toHaveBeenCalledTimes(1)
|
|
13
|
+
|
|
14
|
+
// test twice (should not have changed because we did not change the value)
|
|
15
|
+
await checkbox.trigger('click')
|
|
16
|
+
expect(onUpdateValue).toHaveBeenLastCalledWith(true)
|
|
17
|
+
expect(onUpdateValue).toHaveBeenCalledTimes(2)
|
|
18
|
+
|
|
19
|
+
onUpdateValue.mockReset()
|
|
20
|
+
checkbox = mount(() => <NCheckbox value={true} onUpdateValue={onUpdateValue} />).get('input')
|
|
21
|
+
|
|
22
|
+
await checkbox.trigger('click')
|
|
23
|
+
expect(onUpdateValue).toHaveBeenLastCalledWith(false)
|
|
24
|
+
expect(onUpdateValue).toHaveBeenCalledTimes(1)
|
|
25
|
+
|
|
26
|
+
// test twice (should not have changed because we did not change the value)
|
|
27
|
+
await checkbox.trigger('click')
|
|
28
|
+
expect(onUpdateValue).toHaveBeenLastCalledWith(false)
|
|
29
|
+
expect(onUpdateValue).toHaveBeenCalledTimes(2)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('does not call onUpdateValue when disabled', async () => {
|
|
33
|
+
const onUpdateValue = vi.fn()
|
|
34
|
+
|
|
35
|
+
const checkbox = mount(() => <NCheckbox disabled onUpdateValue={onUpdateValue} />).get('input')
|
|
36
|
+
await checkbox.trigger('click')
|
|
37
|
+
expect(onUpdateValue).not.toHaveBeenCalled()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": {},
|
|
3
|
+
"subtitle": {},
|
|
4
|
+
"text": {
|
|
5
|
+
"loading-search-results": "Ergebnisse werden geladen...",
|
|
6
|
+
"no-search-results": "Keine Ergebnisse für «{input}».",
|
|
7
|
+
"drag-n-drop-files": "Datei auswählen oder per Drag & Drop hinzufügen. | Bis zu {n} Dateien auswählen oder per Drag & Drop hinzufügen.",
|
|
8
|
+
"files-selected": "Keine Datei ausgewählt. | Eine Datei ausgewählt. | {n} Dateien ausgewählt."
|
|
9
|
+
},
|
|
10
|
+
"property": {},
|
|
11
|
+
"term": {},
|
|
12
|
+
"action": {
|
|
13
|
+
"search": "Suchen",
|
|
14
|
+
"select": "Auswählen",
|
|
15
|
+
"remove": "Löschen",
|
|
16
|
+
"cancel": "Abbrechen",
|
|
17
|
+
"all-right": "Alles klar",
|
|
18
|
+
"proceed": "Fortfahren",
|
|
19
|
+
"save": "Speichern",
|
|
20
|
+
"clear-files": "Auswahl löschen."
|
|
21
|
+
},
|
|
22
|
+
"enum": {},
|
|
23
|
+
"error": {
|
|
24
|
+
"file-type": "Eine Datei wurde nicht hinzugefügt, da sie im falschen Format ist. | {n} Dateien wurden nicht hinzugefügt, da sie im falschen Format sind.",
|
|
25
|
+
"file-size": "Eine Datei wurde nicht hinzugefügt, da sie zu gross ist. | {n} Dateien wurden nicht hinzugefügt, da sie zu gross sind.",
|
|
26
|
+
"too-many-files": "Es kann nur eine Datei hinzugefügt werden. | Es können nicht mehr als {n} Dateien hinzugefügt werden."
|
|
27
|
+
},
|
|
28
|
+
"validation": {
|
|
29
|
+
"rules": {
|
|
30
|
+
"email": "Dieses Feld muss eine gültige Email-Adresse sein.",
|
|
31
|
+
"integer": "Dieses Feld muss eine Ganzzahl sein.",
|
|
32
|
+
"length": {
|
|
33
|
+
"min": "Der Text darf nicht weniger als {min} Zeichen enthalten.",
|
|
34
|
+
"max": "Der Text darf nicht mehr als {max} Zeichen enthalten.",
|
|
35
|
+
"min-max": "Der Text muss zwischen {min} und {max} Zeichen enthalten."
|
|
36
|
+
},
|
|
37
|
+
"matches": "Die Felder stimmen nicht überein.",
|
|
38
|
+
"number-range": {
|
|
39
|
+
"nan": "Dieses Feld muss eine Zahl sein.",
|
|
40
|
+
"min": "Die Zahl darf nicht kleiner als {min} sein.",
|
|
41
|
+
"max": "Die Zahl darf nicht grösser als {max} sein.",
|
|
42
|
+
"min-max": "Die Zahl muss im Bereich von {min} bis {max} liegen."
|
|
43
|
+
},
|
|
44
|
+
"option": "Dieses Feld muss eine Option aus der Liste sein.",
|
|
45
|
+
"password": {
|
|
46
|
+
"to-short": "Das Passwort muss aus mindestens 8 Zeichen bestehen.",
|
|
47
|
+
"no-lowercase": "Das Passwort muss einen Kleinbuchstaben enthalten.",
|
|
48
|
+
"no-uppercase": "Das Passwort muss einen Grossbuchstaben enthalten.",
|
|
49
|
+
"no-digits": "Das Passwort muss eine Ziffer enthalten.",
|
|
50
|
+
"no-special-chars": "Das Passwort muss ein Sonderzeichen enthalten.",
|
|
51
|
+
"unknown": "Dieses Feld erfüllt nicht die Anforderungen für ein Passwort."
|
|
52
|
+
},
|
|
53
|
+
"phone": "Dieses Feld muss eine gültige Telefonnummer sein.",
|
|
54
|
+
"required": "Dieses Feld ist ein Pflichtfeld.",
|
|
55
|
+
"regex": "Dieses Feld entspricht nicht dem geforderten Format."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": {},
|
|
3
|
+
"subtitle": {},
|
|
4
|
+
"text": {
|
|
5
|
+
"loading-search-results": "Loading results...",
|
|
6
|
+
"no-search-results": "No results found for «{input}».",
|
|
7
|
+
"drag-n-drop-files": "Select a file or add it by drag & drop. | Select up to {n} files or add them by drag & drop.",
|
|
8
|
+
"files-selected": "No file selected. | One file selected. | {n} files selected."
|
|
9
|
+
},
|
|
10
|
+
"property": {},
|
|
11
|
+
"term": {},
|
|
12
|
+
"action": {
|
|
13
|
+
"search": "Search",
|
|
14
|
+
"select": "Select",
|
|
15
|
+
"remove": "Delete",
|
|
16
|
+
"cancel": "Cancel",
|
|
17
|
+
"all-right": "All right",
|
|
18
|
+
"proceed": "Continue",
|
|
19
|
+
"save": "Save",
|
|
20
|
+
"clear-files": "Clear. | Clear all."
|
|
21
|
+
},
|
|
22
|
+
"enum": {},
|
|
23
|
+
"error": {
|
|
24
|
+
"file-type": "One file has not been added as it is in the wrong format. | {n} files have not been added as they are in the wrong format.",
|
|
25
|
+
"file-size": "One file has not been added as it exceeds the maximum file size. | {n} files have not been added as they exceed the maximum file size.",
|
|
26
|
+
"too-many-files": "Only one file can be added. | Can not add more than {max} files."
|
|
27
|
+
},
|
|
28
|
+
"validation": {
|
|
29
|
+
"rules": {
|
|
30
|
+
"email": "This field must be a valid email address.",
|
|
31
|
+
"integer": "This field must be an integer.",
|
|
32
|
+
"length": {
|
|
33
|
+
"min": "The text must not have less than {min} characters.",
|
|
34
|
+
"max": "The text must not have more than {max} characters.",
|
|
35
|
+
"min-max": "The text must have a length between {min} and {max} characters."
|
|
36
|
+
},
|
|
37
|
+
"matches": "The fields don't match.",
|
|
38
|
+
"number-range": {
|
|
39
|
+
"nan": "This filed must be a number.",
|
|
40
|
+
"min": "The number must not be smaller than {min}.",
|
|
41
|
+
"max": "The number must not be bigger than {max}.",
|
|
42
|
+
"min-max": "The number must be in the range from {min} to {max}."
|
|
43
|
+
},
|
|
44
|
+
"option": "This field has to be an option from the list.",
|
|
45
|
+
"password": {
|
|
46
|
+
"to-short": "The password must have at least 8 characters.",
|
|
47
|
+
"no-lowercase": "The password must contain a lowercase letter.",
|
|
48
|
+
"no-uppercase": "The password must contain an uppercase letter.",
|
|
49
|
+
"no-digits": "The password must contain a digit.",
|
|
50
|
+
"no-special-chars": "The password must contain a special character.",
|
|
51
|
+
"unknown": "This field does not match the required password format."
|
|
52
|
+
},
|
|
53
|
+
"phone": "This field must be a valid phone number.",
|
|
54
|
+
"required": "This field is required.",
|
|
55
|
+
"regex": "This field does not conform to the required format."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Nullish } from '../utils/utils'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @see {@link trsl}
|
|
5
|
+
*/
|
|
6
|
+
export type TranslationFunction = typeof trsl
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @see {@link trslc}
|
|
10
|
+
*/
|
|
11
|
+
export type TranslationCountFunction = typeof trslc
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A `TranslationProvider` has to implement the two functions `trsl` and `trslc`.
|
|
15
|
+
*/
|
|
16
|
+
export type TranslationProvider = {
|
|
17
|
+
trsl: TranslationFunction
|
|
18
|
+
trslc: TranslationCountFunction
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let provider: TranslationProvider | undefined = undefined
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Registeres a new translation provider for vue-collection.
|
|
25
|
+
* The translation provider should contain all vue-collection
|
|
26
|
+
* texts located under `i18n/<lang>/vue-collection.json`.
|
|
27
|
+
* @param newProvider
|
|
28
|
+
*/
|
|
29
|
+
export function registerTranslationProvider(newProvider: TranslationProvider): void {
|
|
30
|
+
provider = newProvider
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Translates the specified key with the according message.
|
|
35
|
+
* @param key the key to translate.
|
|
36
|
+
* @param params formatting parameters for the message.
|
|
37
|
+
* @returns the translated message.
|
|
38
|
+
*/
|
|
39
|
+
export function trsl(key: string, params?: Record<string, unknown>): string {
|
|
40
|
+
return provider?.trsl(key, params) ?? key
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Translates the specified key using pluralization.
|
|
45
|
+
* The provided `count`is automatically passed as the parameter `n` to the translation library.
|
|
46
|
+
* @param key the key to translate.
|
|
47
|
+
* @param count the count used for the pluralization.
|
|
48
|
+
* @param params formatting parameters for the message.
|
|
49
|
+
* @returns the translated message.
|
|
50
|
+
* @see trsl
|
|
51
|
+
*/
|
|
52
|
+
export function trslc(key: string, count: Nullish<number>, params?: Record<string, unknown>): string {
|
|
53
|
+
return provider?.trslc(key, count, params) ?? key
|
|
54
|
+
}
|
package/src/lib/index.ts
ADDED
package/src/lib/jsx.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { Id } from '../identifiable'
|
|
3
|
+
|
|
4
|
+
const one = { id: 'id1', label: 'one' }
|
|
5
|
+
const two = { id: 'id2', label: 'two' }
|
|
6
|
+
const three = { id: 'id3', label: 'three' }
|
|
7
|
+
const four = { id: 'id4', label: 'four' }
|
|
8
|
+
const all = [one, two, three, four]
|
|
9
|
+
|
|
10
|
+
describe('identifiable', () => {
|
|
11
|
+
test('find', () => {
|
|
12
|
+
expect(Id.find(all, 'id1')).toMatchObject(one)
|
|
13
|
+
expect(Id.find(all, 'id3')).toMatchObject(three)
|
|
14
|
+
// check if the first is picked if multiple
|
|
15
|
+
expect(Id.find([...all, { id: 'id2', label: 'new' }], 'id2')).toMatchObject(two)
|
|
16
|
+
// check if undefined is returned if not found
|
|
17
|
+
expect(Id.find(all, 'id')).toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('contains', () => {
|
|
21
|
+
expect(Id.contains(all, 'id1')).toBe(true)
|
|
22
|
+
expect(Id.contains(all, 'id3')).toBe(true)
|
|
23
|
+
expect(Id.contains(all, 'id5')).toBe(false)
|
|
24
|
+
expect(Id.contains(all, 'id')).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('insert', () => {
|
|
28
|
+
const before = [one, two, three]
|
|
29
|
+
let modify = [...before]
|
|
30
|
+
|
|
31
|
+
let after = Id.insert(modify, two, one)
|
|
32
|
+
// Check if array is still the same
|
|
33
|
+
expect(after).toMatchObject(before)
|
|
34
|
+
// Check if the reference is the same
|
|
35
|
+
expect(after).toBe(modify)
|
|
36
|
+
|
|
37
|
+
modify = [...before]
|
|
38
|
+
after = Id.insert(modify, four, three)
|
|
39
|
+
expect(after).toMatchObject([one, two, three, four])
|
|
40
|
+
|
|
41
|
+
modify = [...before]
|
|
42
|
+
const twoModified = { id: 'id2', label: 'twoooo' }
|
|
43
|
+
after = Id.insert(modify, twoModified)
|
|
44
|
+
expect(after).toMatchObject([one, twoModified, three])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('remove', () => {
|
|
48
|
+
const before = [one, two, three]
|
|
49
|
+
let modify = [...before]
|
|
50
|
+
|
|
51
|
+
let after = Id.remove(modify, 'id')
|
|
52
|
+
// Check if array is still the same
|
|
53
|
+
expect(after).toMatchObject(before)
|
|
54
|
+
// Check if the reference is the same
|
|
55
|
+
expect(after).toBe(modify)
|
|
56
|
+
|
|
57
|
+
modify = [...before]
|
|
58
|
+
after = Id.remove(modify, 'id2')
|
|
59
|
+
expect(after).toMatchObject([one, three])
|
|
60
|
+
|
|
61
|
+
modify = [one, two, three, four]
|
|
62
|
+
after = Id.remove(modify, 'id1', 'id3')
|
|
63
|
+
expect(after).toMatchObject([two, four])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('areSameArrays', () => {
|
|
67
|
+
const allDifferent = all.map(item => ({ ...item, label: 'different' }))
|
|
68
|
+
expect(Id.areSameArrays(all, allDifferent)).toBe(true)
|
|
69
|
+
expect(Id.areSameArrays(all, allDifferent.slice(1))).toBe(false)
|
|
70
|
+
expect(Id.areSameArrays(all, allDifferent.reverse())).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { trsl } from '../../i18n'
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { email, external, matches, option, password, regex, required } from '../validation'
|
|
4
|
+
|
|
5
|
+
const expectValid = (test: unknown) => expect(test).toMatchObject({ isValid: true })
|
|
6
|
+
const expectInvalid = (test: unknown, errKey: string) =>
|
|
7
|
+
expect(test).toMatchObject({ isValid: false, errorMessage: trsl(`vue-collection.validation.rules.${errKey}`) })
|
|
8
|
+
|
|
9
|
+
describe('validation', () => {
|
|
10
|
+
test('required', () => {
|
|
11
|
+
expectValid(required('_'))
|
|
12
|
+
expectValid(required(' i '))
|
|
13
|
+
expectValid(required('hellooh'))
|
|
14
|
+
expectValid(required('`'))
|
|
15
|
+
|
|
16
|
+
expectInvalid(required(undefined), 'required')
|
|
17
|
+
expectInvalid(required(null), 'required')
|
|
18
|
+
expectInvalid(required(''), 'required')
|
|
19
|
+
expectInvalid(required(' '), 'required')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('email', () => {
|
|
23
|
+
expectValid(email(''))
|
|
24
|
+
expectValid(email('_@_.ch'))
|
|
25
|
+
expectValid(email('dfadfasd.dfad.fdak@_.ch'))
|
|
26
|
+
expectValid(email('dfadfasd.dfad.fdak@dfdaf.chdl.ch'))
|
|
27
|
+
expectValid(email('deslek-djsl@dfdaf-cas.ch'))
|
|
28
|
+
|
|
29
|
+
expectInvalid(email('a'), 'email')
|
|
30
|
+
expectInvalid(email('a@'), 'email')
|
|
31
|
+
expectInvalid(email('a@d'), 'email')
|
|
32
|
+
expectInvalid(email('a@d.c'), 'email')
|
|
33
|
+
expectInvalid(email('a.ch'), 'email')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('password', () => {
|
|
37
|
+
expectValid(password(''))
|
|
38
|
+
expectValid(password('Password1+'))
|
|
39
|
+
expectValid(password('+Password1'))
|
|
40
|
+
expectValid(password('1+Password'))
|
|
41
|
+
expectValid(password('assword1+P'))
|
|
42
|
+
expectValid(password('12p+D678'))
|
|
43
|
+
|
|
44
|
+
expectInvalid(password('p'), 'password.to-short')
|
|
45
|
+
expectInvalid(password('Pword1+'), 'password.to-short')
|
|
46
|
+
expectInvalid(password('PASSWORD1+'), 'password.no-lowercase')
|
|
47
|
+
expectInvalid(password('password1+'), 'password.no-uppercase')
|
|
48
|
+
expectInvalid(password('Password+'), 'password.no-digits')
|
|
49
|
+
expectInvalid(password('Password1'), 'password.no-special-chars')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('matches', () => {
|
|
53
|
+
const testValid = [undefined, null, '', '1', 'hello', 'So cooool']
|
|
54
|
+
testValid.forEach(value => {
|
|
55
|
+
expectValid(matches(value)(value))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// This rule does not allow the input to be falsy, always has to match.
|
|
59
|
+
const testInvalid = ['', '1', 'hi', 'noice', 'not-null', 'not-null', 'not-null']
|
|
60
|
+
const checkInvalid = ['ho', '11', 'HI', 'noices', '', null, undefined]
|
|
61
|
+
|
|
62
|
+
testInvalid.forEach((value, index) => {
|
|
63
|
+
expectInvalid(matches(value)(checkInvalid[index]), 'matches')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('regex', () => {
|
|
68
|
+
expectValid(regex(/\d+/)('123'))
|
|
69
|
+
expectValid(regex(/\d*/)(null))
|
|
70
|
+
expectValid(regex(/\d+/)(null))
|
|
71
|
+
|
|
72
|
+
expectInvalid(regex(/\d+/)('abc'), 'regex')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('option', () => {
|
|
76
|
+
const options1 = ['a', 'b', 'ab', 'ac', 'a']
|
|
77
|
+
expectValid(option(options1)(null))
|
|
78
|
+
expectValid(option(options1)('a'))
|
|
79
|
+
expectValid(option(options1)('b'))
|
|
80
|
+
expectValid(option(options1)('ab'))
|
|
81
|
+
|
|
82
|
+
expectInvalid(option(options1)('c'), 'option')
|
|
83
|
+
expectInvalid(option(options1)('A'), 'option')
|
|
84
|
+
expectInvalid(option(options1)('aB'), 'option')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('external', () => {
|
|
88
|
+
expectValid(external(true, trsl('vue-collection.validation.rules.phone'))('hi'))
|
|
89
|
+
expectValid(external(false, trsl('vue-collection.validation.rules.phone'))(null))
|
|
90
|
+
expectInvalid(external(false, trsl('vue-collection.validation.rules.phone'))('hi'), 'phone')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { computed, ref, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type TWBreakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
4
|
+
|
|
5
|
+
export const breakpoints: Readonly<Record<TWBreakpoint, number>> = {
|
|
6
|
+
sm: 640,
|
|
7
|
+
md: 768,
|
|
8
|
+
lg: 1024,
|
|
9
|
+
xl: 1280,
|
|
10
|
+
'2xl': 1536,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const bodyWidth = ref(document.body.clientWidth)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function has to be called once in the app two ensure that the breakpoint utilities actually update.
|
|
17
|
+
* It sets a `window.onresize` listener.
|
|
18
|
+
*/
|
|
19
|
+
export function addDocumentResizeEventListener(): void {
|
|
20
|
+
window.onresize = () => {
|
|
21
|
+
bodyWidth.value = document.body.clientWidth
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns a ref whose value is `true` as long as the `document.body.clientWitdh` is above the specified breakpoint.
|
|
27
|
+
*/
|
|
28
|
+
export function isWidthBreakpoint(breakpoint: TWBreakpoint): ComputedRef<boolean> {
|
|
29
|
+
switch (breakpoint) {
|
|
30
|
+
case 'sm':
|
|
31
|
+
return isWidthSm
|
|
32
|
+
case 'md':
|
|
33
|
+
return isWidthMd
|
|
34
|
+
case 'lg':
|
|
35
|
+
return isWidthLg
|
|
36
|
+
case 'xl':
|
|
37
|
+
return isWidthXl
|
|
38
|
+
case '2xl':
|
|
39
|
+
return isWidth2xl
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const isWidth2xl = computed(() => bodyWidth.value > breakpoints['2xl'])
|
|
44
|
+
export const isWidthXl = computed(() => bodyWidth.value > breakpoints.xl)
|
|
45
|
+
export const isWidthLg = computed(() => bodyWidth.value > breakpoints.lg)
|
|
46
|
+
export const isWidthMd = computed(() => bodyWidth.value > breakpoints.md)
|
|
47
|
+
export const isWidthSm = computed(() => bodyWidth.value > breakpoints.sm)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// eslint-disable @typescript-eslint/explicit-module-boundary-types
|
|
2
|
+
// eslint-disable-next-line vue/prefer-import-from-vue
|
|
3
|
+
import type { LooseRequired } from '@vue/shared'
|
|
4
|
+
import {
|
|
5
|
+
defineComponent,
|
|
6
|
+
reactive,
|
|
7
|
+
toRef,
|
|
8
|
+
type ComponentObjectPropsOptions,
|
|
9
|
+
type ExtractPropTypes,
|
|
10
|
+
type RenderFunction,
|
|
11
|
+
type SetupContext,
|
|
12
|
+
type ToRefs,
|
|
13
|
+
type UnwrapNestedRefs,
|
|
14
|
+
type Slots,
|
|
15
|
+
toRefs,
|
|
16
|
+
ref,
|
|
17
|
+
} from 'vue'
|
|
18
|
+
import type { AnyObject } from './utils'
|
|
19
|
+
|
|
20
|
+
type Data = Record<string, unknown>
|
|
21
|
+
export type Props<T extends Data = Data> = ComponentObjectPropsOptions<T>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `ExtractedProps` maps a prop object to the props, which are received in the `setup` function of a components.
|
|
25
|
+
*/
|
|
26
|
+
export type ExtractedProps<T extends Props> = Readonly<LooseRequired<Readonly<ExtractPropTypes<T>>>>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Components should be created using this helper function.
|
|
30
|
+
* It only takes three arguments, the name and the props of the component and the setup function.
|
|
31
|
+
* All other arguments which the {@link defineComponent} method of vue may provide,
|
|
32
|
+
* should not be used for a better consistency across all components.
|
|
33
|
+
* @param name the name of the component, should be more than one word.
|
|
34
|
+
* @param props the props of the component.
|
|
35
|
+
* @param setup the setup function, which will be called when the component is mounted.
|
|
36
|
+
* @returns the created vue-component.
|
|
37
|
+
*/
|
|
38
|
+
export function createComponent<T extends Props>(
|
|
39
|
+
name: string,
|
|
40
|
+
props: T,
|
|
41
|
+
setup: (props: ExtractedProps<T>, context: SetupContext<never[]>) => RenderFunction | Promise<RenderFunction>
|
|
42
|
+
) {
|
|
43
|
+
// Vue 3.5's defineComponent has strict type requirements that don't align with our simplified API.
|
|
44
|
+
// The type assertion is necessary because the generic setup function signature doesn't match
|
|
45
|
+
// Vue's complex overloaded defineComponent types, even though the runtime behavior is correct.
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
return defineComponent({ name, props, emits: [], setup } as any)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* When using this function, the created component will make available all props
|
|
52
|
+
* specifiedin `slotPropKeys` as slot. In this way, they can be used by either setting
|
|
53
|
+
* the prop directly or by using a slot with the same name. This is useful for older
|
|
54
|
+
* components (in `.vue` files), because they are dependent on slots.
|
|
55
|
+
* @see {@link createComponent}
|
|
56
|
+
*/
|
|
57
|
+
export function createComponentWithSlots<T extends Props>(
|
|
58
|
+
name: string,
|
|
59
|
+
props: T,
|
|
60
|
+
slotPropKeys: SlotPropsKeys<ExtractedProps<T>>,
|
|
61
|
+
setup: (props: ExtractedProps<T>, context: SetupContext<never[]>) => RenderFunction | Promise<RenderFunction>
|
|
62
|
+
) {
|
|
63
|
+
const newSetup: typeof setup = (props, context) => {
|
|
64
|
+
const slottedProps = createSlotProps(props, context.slots, ...slotPropKeys)
|
|
65
|
+
return setup(slottedProps, context)
|
|
66
|
+
}
|
|
67
|
+
return createComponent(name, props, newSetup)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Views should be created using this helper function. Views are special components, which don't have props.
|
|
72
|
+
* They are often the parent objects in a view hierarchy and contain many components.
|
|
73
|
+
* This function is syntactic sugar to create views and just calls {@link createComponent}.
|
|
74
|
+
* @param name the name of the component, should be more than one word.
|
|
75
|
+
* @param setup the setup function, which will be called when the component is mounted.
|
|
76
|
+
* @returns the created vue-component.
|
|
77
|
+
*/
|
|
78
|
+
export function createView(
|
|
79
|
+
name: string,
|
|
80
|
+
setup: (context: SetupContext<never[]>) => RenderFunction | Promise<RenderFunction>
|
|
81
|
+
) {
|
|
82
|
+
return defineComponent({ name, emits: [], setup: (props, context) => setup(context) })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extracts props from another prop object and returns a reactive object with the specified props.
|
|
87
|
+
* @param props the props to extract from
|
|
88
|
+
* @param keys the keys to extract from the props
|
|
89
|
+
* @returns the new object with the specified props
|
|
90
|
+
* @example
|
|
91
|
+
* const parentProps = { title: 'hi', text: 'ho' }
|
|
92
|
+
* const childProps = extractProps(parentProps, 'title')
|
|
93
|
+
* console.log(childProps) // { title: 'hi' }
|
|
94
|
+
*/
|
|
95
|
+
export function extractProps<T extends Record<string, unknown>>(
|
|
96
|
+
props: T,
|
|
97
|
+
...keys: (keyof T)[]
|
|
98
|
+
): UnwrapNestedRefs<Partial<ToRefs<T>>> {
|
|
99
|
+
const partial: Partial<ToRefs<T>> = {}
|
|
100
|
+
for (const key of keys) partial[key] = toRef(props, key)
|
|
101
|
+
return reactive(partial)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* ---------- Slot Helpers ----------
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
type SlotFunction<Args extends any[] = any[]> = (...args: Args) => JSX.Element
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Filters object T to only have properties of type K.
|
|
113
|
+
*/
|
|
114
|
+
type FilterObject<T, K> = {
|
|
115
|
+
[P in keyof T as T[P] extends K ? P : never]: T[P]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type SlotPropsKeys<T extends AnyObject> = (keyof FilterObject<T, SlotFunction | undefined>)[]
|
|
119
|
+
|
|
120
|
+
function createSlotProps<T extends AnyObject, U extends SlotPropsKeys<T>>(props: T, slots: Slots, ...keys: U): T {
|
|
121
|
+
// create refs, don't touch all props which are not slots
|
|
122
|
+
const newProps = toRefs(props)
|
|
123
|
+
keys.forEach(key => {
|
|
124
|
+
// if a slot is set once, it is basically always set. The changing content is not a problem as it is inside the function.
|
|
125
|
+
const slot = slots[key as string]
|
|
126
|
+
// if the slot is set, overwrite the props
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
if (slot) newProps[key] = ((...args: any) => <>{slot?.(...args)}</>) as any
|
|
129
|
+
})
|
|
130
|
+
return ref(newProps).value
|
|
131
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type PromiseResolve<T> = (value: T | PromiseLike<T>) => void
|
|
2
|
+
type PromiseReject = (reason?: unknown) => void
|
|
3
|
+
|
|
4
|
+
export type DeferredPromise<T> = {
|
|
5
|
+
promise: Promise<T>
|
|
6
|
+
resolve: PromiseResolve<T>
|
|
7
|
+
reject: PromiseReject
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utlitity to return a deferred promise, which can be resolved from outside.
|
|
12
|
+
* @returns promise, resolve and reject
|
|
13
|
+
*/
|
|
14
|
+
export function deferred<T>(): DeferredPromise<T> {
|
|
15
|
+
let resolve!: PromiseResolve<T>
|
|
16
|
+
let reject!: PromiseReject
|
|
17
|
+
|
|
18
|
+
const promise = new Promise<T>((_resolve, _reject) => {
|
|
19
|
+
resolve = _resolve
|
|
20
|
+
reject = _reject
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
promise,
|
|
25
|
+
resolve,
|
|
26
|
+
reject,
|
|
27
|
+
}
|
|
28
|
+
}
|