fds-vue-core 8.2.4 → 8.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/components.d.ts +44 -42
- package/dist/components/Form/FdsSsn/FdsSsn.vue.d.ts +33 -0
- package/dist/components/Form/FdsSsn/ssnMask.d.ts +19 -0
- package/dist/components/Form/FdsSsn/types.d.ts +24 -0
- package/dist/fds-vue-core.cjs.js +784 -305
- package/dist/fds-vue-core.cjs.js.map +1 -1
- package/dist/fds-vue-core.css +1 -1
- package/dist/fds-vue-core.es.js +785 -306
- package/dist/fds-vue-core.es.js.map +1 -1
- package/dist/helpers/validateSsn.d.ts +16 -0
- package/dist/index.d.ts +5 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +17 -16
- package/src/components/FdsDevMode/FdsDevModeStorage.vue +1 -1
- package/src/components/Form/FdsCheckbox/FdsCheckbox.vue +3 -6
- package/src/components/Form/FdsInput/FdsInput.vue +3 -3
- package/src/components/Form/FdsRadio/FdsRadio.vue +1 -1
- package/src/components/Form/FdsSelect/FdsSelect.vue +2 -2
- package/src/components/Form/FdsSsn/FdsSsn.stories.ts +131 -0
- package/src/components/Form/FdsSsn/FdsSsn.vue +133 -0
- package/src/components/Form/FdsSsn/ssnMask.ts +20 -0
- package/src/components/Form/FdsSsn/types.ts +36 -0
- package/src/components/Form/FdsTextarea/FdsTextarea.vue +2 -2
- package/src/helpers/validateSsn.ts +63 -0
- package/src/index.ts +16 -0
- package/src/lang/en.json +2 -0
- package/src/lang/sv.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fds-vue-core",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.1",
|
|
4
4
|
"description": "FDS Vue Core Component Library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/fds-vue-core.cjs.js",
|
|
@@ -52,28 +52,29 @@
|
|
|
52
52
|
"date-fns": "4.4.0",
|
|
53
53
|
"imask": "7.6.1",
|
|
54
54
|
"libphonenumber-js": "1.13.7",
|
|
55
|
-
"
|
|
55
|
+
"personnummer": "3.2.1",
|
|
56
|
+
"tailwindcss": "4.3.3"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@chromatic-com/storybook": "5.2.1",
|
|
59
60
|
"@intlify/unplugin-vue-i18n": "11.2.4",
|
|
60
|
-
"@storybook/addon-a11y": "10.4
|
|
61
|
-
"@storybook/addon-docs": "10.4
|
|
62
|
-
"@storybook/addon-vitest": "10.4
|
|
63
|
-
"@storybook/vue3": "10.4
|
|
64
|
-
"@storybook/vue3-vite": "10.4
|
|
65
|
-
"@tailwindcss/vite": "4.3.
|
|
61
|
+
"@storybook/addon-a11y": "10.5.4",
|
|
62
|
+
"@storybook/addon-docs": "10.5.4",
|
|
63
|
+
"@storybook/addon-vitest": "10.5.4",
|
|
64
|
+
"@storybook/vue3": "10.5.4",
|
|
65
|
+
"@storybook/vue3-vite": "10.5.4",
|
|
66
|
+
"@tailwindcss/vite": "4.3.3",
|
|
66
67
|
"@types/node": "22.16.5",
|
|
67
|
-
"@vitejs/plugin-vue": "6.0.
|
|
68
|
-
"@vitest/browser": "3.2.
|
|
69
|
-
"fg-devkit": "1.12.
|
|
70
|
-
"storybook": "10.4
|
|
71
|
-
"tsx": "4.
|
|
68
|
+
"@vitejs/plugin-vue": "6.0.8",
|
|
69
|
+
"@vitest/browser": "3.2.7",
|
|
70
|
+
"fg-devkit": "1.12.7",
|
|
71
|
+
"storybook": "10.5.4",
|
|
72
|
+
"tsx": "4.23.1",
|
|
72
73
|
"vite": "7.3.5",
|
|
73
74
|
"vite-plugin-dts": "4.5.4",
|
|
74
|
-
"vite-plugin-vue-devtools": "8.1
|
|
75
|
-
"vitest": "4.1.
|
|
76
|
-
"vue": "3.5.
|
|
75
|
+
"vite-plugin-vue-devtools": "8.2.1",
|
|
76
|
+
"vitest": "4.1.10",
|
|
77
|
+
"vue": "3.5.40",
|
|
77
78
|
"vue-i18n": "11.4.6"
|
|
78
79
|
},
|
|
79
80
|
"knip": {
|
|
@@ -477,7 +477,7 @@ const deleteCookieEntry = () => {
|
|
|
477
477
|
:type="action.input.type ?? 'text'"
|
|
478
478
|
:modelValue="getActionInputValue(index, action.input.defaultValue)"
|
|
479
479
|
:placeholder="action.input.placeholder"
|
|
480
|
-
@update:modelValue="setActionInputValue(index, $event)"
|
|
480
|
+
@update:modelValue="setActionInputValue(index, $event ?? '')"
|
|
481
481
|
/>
|
|
482
482
|
</div>
|
|
483
483
|
</div>
|
|
@@ -10,10 +10,7 @@ defineOptions({
|
|
|
10
10
|
|
|
11
11
|
// Support v-model as boolean (single checkbox) or array (checkbox group)
|
|
12
12
|
// Also support :checked prop for backward compatibility
|
|
13
|
-
const modelValue = defineModel<boolean | Array<string | number | boolean | null>>(
|
|
14
|
-
default: undefined,
|
|
15
|
-
required: false,
|
|
16
|
-
})
|
|
13
|
+
const modelValue = defineModel<boolean | Array<string | number | boolean | null>>()
|
|
17
14
|
|
|
18
15
|
const props = withDefaults(defineProps<FdsCheckboxProps>(), {
|
|
19
16
|
label: undefined,
|
|
@@ -133,13 +130,13 @@ const internalChecked = computed({
|
|
|
133
130
|
newArray.splice(index, 1)
|
|
134
131
|
}
|
|
135
132
|
}
|
|
136
|
-
modelValue.value = newArray
|
|
133
|
+
modelValue.value = newArray as boolean | Array<string | number | boolean | null>
|
|
137
134
|
emit('update:checked', checked)
|
|
138
135
|
emit('change', checked)
|
|
139
136
|
emit('input', checked)
|
|
140
137
|
} else {
|
|
141
138
|
// Boolean mode
|
|
142
|
-
modelValue.value = checked
|
|
139
|
+
modelValue.value = checked as boolean | Array<string | number | boolean | null>
|
|
143
140
|
emit('update:checked', checked)
|
|
144
141
|
emit('change', checked)
|
|
145
142
|
emit('input', checked)
|
|
@@ -14,7 +14,7 @@ defineOptions({
|
|
|
14
14
|
inheritAttrs: false,
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
const modelValue = defineModel<string>({ default:
|
|
17
|
+
const modelValue = defineModel<string>({ default: '' })
|
|
18
18
|
|
|
19
19
|
const props = withDefaults(defineProps<FdsInputProps>(), {
|
|
20
20
|
value: undefined,
|
|
@@ -182,7 +182,7 @@ const internalValue = computed({
|
|
|
182
182
|
get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
|
|
183
183
|
set: (newValue: string) => {
|
|
184
184
|
if (modelValue.value !== undefined) {
|
|
185
|
-
modelValue.value = newValue
|
|
185
|
+
modelValue.value = newValue as string
|
|
186
186
|
}
|
|
187
187
|
emit('update:value', newValue)
|
|
188
188
|
},
|
|
@@ -252,7 +252,7 @@ const createMask = () => {
|
|
|
252
252
|
|
|
253
253
|
maskInstance.on('accept', () => {
|
|
254
254
|
if (maskInstance) {
|
|
255
|
-
internalValue.value = maskInstance.value
|
|
255
|
+
internalValue.value = maskInstance.value as string
|
|
256
256
|
}
|
|
257
257
|
})
|
|
258
258
|
}
|
|
@@ -7,7 +7,7 @@ defineOptions({
|
|
|
7
7
|
inheritAttrs: false,
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
const modelValue = defineModel<string | number | boolean | null>(
|
|
10
|
+
const modelValue = defineModel<string | number | boolean | null>()
|
|
11
11
|
|
|
12
12
|
const props = withDefaults(defineProps<FdsRadioProps>(), {
|
|
13
13
|
label: undefined,
|
|
@@ -9,7 +9,7 @@ defineOptions({
|
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
// Support both v-model (modelValue) and :value prop for backward compatibility
|
|
12
|
-
const modelValue = defineModel<string>(
|
|
12
|
+
const modelValue = defineModel<string>()
|
|
13
13
|
|
|
14
14
|
const props = withDefaults(defineProps<FdsSelectProps>(), {
|
|
15
15
|
value: undefined,
|
|
@@ -88,7 +88,7 @@ const internalValue = computed({
|
|
|
88
88
|
set: (newValue: string) => {
|
|
89
89
|
// Update modelValue if it's being used
|
|
90
90
|
if (modelValue.value !== undefined) {
|
|
91
|
-
modelValue.value = newValue
|
|
91
|
+
modelValue.value = newValue as string
|
|
92
92
|
}
|
|
93
93
|
// Always emit update:value for backward compatibility
|
|
94
94
|
emit('update:value', newValue)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import FdsSsn from './FdsSsn.vue'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof FdsSsn> = {
|
|
6
|
+
title: 'FDS/Form/FdsSsn',
|
|
7
|
+
component: FdsSsn,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
label: { control: 'text' },
|
|
11
|
+
meta: { control: 'text' },
|
|
12
|
+
disabled: { control: 'boolean' },
|
|
13
|
+
optional: { control: 'boolean' },
|
|
14
|
+
valid: { control: 'select', options: [undefined, true, false, null] },
|
|
15
|
+
invalidMessage: { control: 'text' },
|
|
16
|
+
allowCoordinationNumber: { control: 'boolean' },
|
|
17
|
+
allowInterimNumber: { control: 'boolean' },
|
|
18
|
+
},
|
|
19
|
+
args: {
|
|
20
|
+
label: 'Personnummer',
|
|
21
|
+
meta: '',
|
|
22
|
+
disabled: false,
|
|
23
|
+
optional: false,
|
|
24
|
+
valid: undefined,
|
|
25
|
+
invalidMessage: 'Ange ett giltigt personnummer',
|
|
26
|
+
allowCoordinationNumber: true,
|
|
27
|
+
allowInterimNumber: true,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default meta
|
|
32
|
+
type Story = StoryObj<typeof meta>
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
render: (args) => ({
|
|
36
|
+
components: { FdsSsn },
|
|
37
|
+
setup() {
|
|
38
|
+
const ssn = ref('')
|
|
39
|
+
const ssnValid = ref<boolean | null>(null)
|
|
40
|
+
return { args, ssn, ssnValid }
|
|
41
|
+
},
|
|
42
|
+
template: `
|
|
43
|
+
<div>
|
|
44
|
+
<FdsSsn
|
|
45
|
+
v-bind="args"
|
|
46
|
+
v-model="ssn"
|
|
47
|
+
@valid="ssnValid = $event"
|
|
48
|
+
/>
|
|
49
|
+
<p class="mt-4 text-sm">v-model: {{ ssn || '—' }}</p>
|
|
50
|
+
<p class="text-sm">valid: {{ ssnValid }}</p>
|
|
51
|
+
</div>
|
|
52
|
+
`,
|
|
53
|
+
}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const InvalidValue: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
valid: false,
|
|
59
|
+
},
|
|
60
|
+
render: (args) => ({
|
|
61
|
+
components: { FdsSsn },
|
|
62
|
+
setup() {
|
|
63
|
+
const ssn = ref('198501011234')
|
|
64
|
+
return { args, ssn }
|
|
65
|
+
},
|
|
66
|
+
template: `
|
|
67
|
+
<FdsSsn v-bind="args" v-model="ssn" />
|
|
68
|
+
`,
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const ValidPersonnummer: Story = {
|
|
73
|
+
render: (args) => ({
|
|
74
|
+
components: { FdsSsn },
|
|
75
|
+
setup() {
|
|
76
|
+
const ssn = ref('198507099805')
|
|
77
|
+
return { args, ssn }
|
|
78
|
+
},
|
|
79
|
+
template: `
|
|
80
|
+
<FdsSsn v-bind="args" v-model="ssn" />
|
|
81
|
+
`,
|
|
82
|
+
}),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const CoordinationNumber: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
allowCoordinationNumber: true,
|
|
88
|
+
},
|
|
89
|
+
render: (args) => ({
|
|
90
|
+
components: { FdsSsn },
|
|
91
|
+
setup() {
|
|
92
|
+
const ssn = ref('198506698805')
|
|
93
|
+
const ssnValid = ref<boolean | null>(null)
|
|
94
|
+
return { args, ssn, ssnValid }
|
|
95
|
+
},
|
|
96
|
+
template: `
|
|
97
|
+
<div>
|
|
98
|
+
<FdsSsn
|
|
99
|
+
v-bind="args"
|
|
100
|
+
v-model="ssn"
|
|
101
|
+
@valid="ssnValid = $event"
|
|
102
|
+
/>
|
|
103
|
+
<p class="mt-4 text-sm">valid: {{ ssnValid }}</p>
|
|
104
|
+
</div>
|
|
105
|
+
`,
|
|
106
|
+
}),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const InterimNumber: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
allowInterimNumber: true,
|
|
112
|
+
},
|
|
113
|
+
render: (args) => ({
|
|
114
|
+
components: { FdsSsn },
|
|
115
|
+
setup() {
|
|
116
|
+
const ssn = ref('19850709T125')
|
|
117
|
+
const ssnValid = ref<boolean | null>(null)
|
|
118
|
+
return { args, ssn, ssnValid }
|
|
119
|
+
},
|
|
120
|
+
template: `
|
|
121
|
+
<div>
|
|
122
|
+
<FdsSsn
|
|
123
|
+
v-bind="args"
|
|
124
|
+
v-model="ssn"
|
|
125
|
+
@valid="ssnValid = $event"
|
|
126
|
+
/>
|
|
127
|
+
<p class="mt-4 text-sm">valid: {{ ssnValid }}</p>
|
|
128
|
+
</div>
|
|
129
|
+
`,
|
|
130
|
+
}),
|
|
131
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, useAttrs, watch } from 'vue'
|
|
3
|
+
import { useFdsI18n } from '../../../plugin/useFdsI18n'
|
|
4
|
+
import FdsInput from '../FdsInput/FdsInput.vue'
|
|
5
|
+
import { getSsnMask, getSsnMaskOptions } from './ssnMask'
|
|
6
|
+
import type { FdsSsnEmits, FdsSsnProps } from './types'
|
|
7
|
+
import { getSsnValidationState, stripSsnSeparators, type SsnValidationOptions } from '../../../helpers/validateSsn'
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
inheritAttrs: false,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const modelValue = defineModel<string>({ default: '' })
|
|
14
|
+
const attrs = useAttrs()
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<FdsSsnProps>(), {
|
|
17
|
+
label: undefined,
|
|
18
|
+
meta: undefined,
|
|
19
|
+
optional: false,
|
|
20
|
+
valid: undefined,
|
|
21
|
+
invalidMessage: undefined,
|
|
22
|
+
id: undefined,
|
|
23
|
+
autocomplete: 'off',
|
|
24
|
+
required: false,
|
|
25
|
+
placeholder: undefined,
|
|
26
|
+
name: undefined,
|
|
27
|
+
autofocus: false,
|
|
28
|
+
readonly: false,
|
|
29
|
+
allowCoordinationNumber: true,
|
|
30
|
+
allowInterimNumber: true,
|
|
31
|
+
disabled: false,
|
|
32
|
+
dataTestid: undefined,
|
|
33
|
+
inputClass: undefined,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const emit = defineEmits<FdsSsnEmits>()
|
|
37
|
+
|
|
38
|
+
const { t } = useFdsI18n()
|
|
39
|
+
|
|
40
|
+
const inputAttrs = computed(() => {
|
|
41
|
+
const { class: _class, style, ...rest } = attrs
|
|
42
|
+
return {
|
|
43
|
+
...rest,
|
|
44
|
+
...(style == null ? {} : { style: style as string | Record<string, unknown> }),
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const forwardedInputProps = computed(() => ({
|
|
49
|
+
id: props.id,
|
|
50
|
+
autocomplete: props.autocomplete,
|
|
51
|
+
required: props.required,
|
|
52
|
+
placeholder: props.placeholder,
|
|
53
|
+
name: props.name,
|
|
54
|
+
autofocus: props.autofocus,
|
|
55
|
+
readonly: props.readonly,
|
|
56
|
+
inputmode: props.allowInterimNumber ? 'text' : 'numeric',
|
|
57
|
+
...inputAttrs.value,
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
const validationOptions = computed(
|
|
61
|
+
(): SsnValidationOptions => ({
|
|
62
|
+
allowCoordinationNumber: props.allowCoordinationNumber,
|
|
63
|
+
allowInterimNumber: props.allowInterimNumber,
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const ssnMask = computed(() => getSsnMask(props.allowInterimNumber))
|
|
68
|
+
const ssnMaskOptions = computed(() => getSsnMaskOptions(props.allowInterimNumber))
|
|
69
|
+
|
|
70
|
+
/** Set after blur; validation does not run on input. */
|
|
71
|
+
const committedValid = ref<boolean | null>(null)
|
|
72
|
+
|
|
73
|
+
const resolvedLabel = computed(() => (props.label === undefined ? t('FdsSsn.label') : props.label))
|
|
74
|
+
|
|
75
|
+
const resolvedInvalidMessage = computed(() =>
|
|
76
|
+
props.invalidMessage === undefined ? t('FdsSsn.invalidSsn') : props.invalidMessage,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const displayValid = computed((): boolean | null | undefined => {
|
|
80
|
+
if (props.valid === false) return false
|
|
81
|
+
if (props.valid === true) return true
|
|
82
|
+
if (committedValid.value === false) return false
|
|
83
|
+
if (committedValid.value === true) return true
|
|
84
|
+
return props.valid
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function normalizeValue() {
|
|
88
|
+
const normalized = stripSsnSeparators(modelValue.value ?? '')
|
|
89
|
+
if (normalized !== (modelValue.value ?? '')) {
|
|
90
|
+
modelValue.value = normalized
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runValidation() {
|
|
95
|
+
const validationState = getSsnValidationState(modelValue.value ?? '', validationOptions.value)
|
|
96
|
+
committedValid.value = validationState
|
|
97
|
+
emit('valid', validationState)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
watch([() => props.allowCoordinationNumber, () => props.allowInterimNumber], () => {
|
|
101
|
+
if (committedValid.value === null) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
runValidation()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
function handleBlur(ev: FocusEvent) {
|
|
108
|
+
normalizeValue()
|
|
109
|
+
runValidation()
|
|
110
|
+
emit('blur', ev)
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<template>
|
|
115
|
+
<div class="w-full">
|
|
116
|
+
<FdsInput
|
|
117
|
+
v-model="modelValue"
|
|
118
|
+
:mask="ssnMask"
|
|
119
|
+
:mask-options="ssnMaskOptions"
|
|
120
|
+
:label="resolvedLabel"
|
|
121
|
+
:meta="meta"
|
|
122
|
+
:valid="displayValid"
|
|
123
|
+
:disabled="disabled"
|
|
124
|
+
:optional="optional"
|
|
125
|
+
:invalid-message="resolvedInvalidMessage"
|
|
126
|
+
:ariaLabel="resolvedLabel"
|
|
127
|
+
:data-testid="dataTestid"
|
|
128
|
+
:class="inputClass"
|
|
129
|
+
v-bind="forwardedInputProps"
|
|
130
|
+
@blur="handleBlur"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** IMask pattern for ÅÅÅÅMMDDKKKK (12 digits, no separator). */
|
|
2
|
+
export const SSN_MASK = '000000000000'
|
|
3
|
+
|
|
4
|
+
/** Alphanumeric mask when interim numbers (letters in num part) are allowed. */
|
|
5
|
+
export const SSN_INTERIM_MASK = 'XXXXXXXXXXXX'
|
|
6
|
+
|
|
7
|
+
export const SSN_INTERIM_MASK_OPTIONS = {
|
|
8
|
+
lazy: true,
|
|
9
|
+
definitions: {
|
|
10
|
+
X: /[0-9A-Za-z]/,
|
|
11
|
+
},
|
|
12
|
+
} as const
|
|
13
|
+
|
|
14
|
+
export function getSsnMask(allowInterimNumber: boolean): string {
|
|
15
|
+
return allowInterimNumber ? SSN_INTERIM_MASK : SSN_MASK
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSsnMaskOptions(allowInterimNumber: boolean) {
|
|
19
|
+
return allowInterimNumber ? SSN_INTERIM_MASK_OPTIONS : { lazy: true }
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FdsInputProps } from '../FdsInput/types'
|
|
2
|
+
|
|
3
|
+
type FdsSsnInputPassthroughProps = Pick<
|
|
4
|
+
FdsInputProps,
|
|
5
|
+
| 'id'
|
|
6
|
+
| 'autocomplete'
|
|
7
|
+
| 'required'
|
|
8
|
+
| 'placeholder'
|
|
9
|
+
| 'name'
|
|
10
|
+
| 'autofocus'
|
|
11
|
+
| 'readonly'
|
|
12
|
+
| 'inputClass'
|
|
13
|
+
>
|
|
14
|
+
|
|
15
|
+
export interface FdsSsnProps extends FdsSsnInputPassthroughProps {
|
|
16
|
+
label?: string
|
|
17
|
+
meta?: string
|
|
18
|
+
optional?: boolean
|
|
19
|
+
valid?: boolean | null
|
|
20
|
+
invalidMessage?: string
|
|
21
|
+
/** Personnummer as shown in the input (12 characters, no separator). */
|
|
22
|
+
modelValue?: string
|
|
23
|
+
allowCoordinationNumber?: boolean
|
|
24
|
+
allowInterimNumber?: boolean
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
dataTestid?: string
|
|
27
|
+
onValid?: ((value: boolean | null) => void) | Array<(value: boolean | null) => void>
|
|
28
|
+
onBlur?: ((event: FocusEvent) => void) | Array<(event: FocusEvent) => void>
|
|
29
|
+
'onUpdate:modelValue'?: ((value: string) => void) | Array<(value: string) => void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FdsSsnEmits {
|
|
33
|
+
'update:modelValue': [value: string]
|
|
34
|
+
valid: [value: boolean | null]
|
|
35
|
+
blur: [ev: FocusEvent]
|
|
36
|
+
}
|
|
@@ -12,7 +12,7 @@ defineOptions({
|
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
// Support both v-model (modelValue) and :value prop for backward compatibility
|
|
15
|
-
const modelValue = defineModel<string>(
|
|
15
|
+
const modelValue = defineModel<string>()
|
|
16
16
|
|
|
17
17
|
const props = withDefaults(defineProps<FdsTextareaProps>(), {
|
|
18
18
|
value: undefined,
|
|
@@ -67,7 +67,7 @@ const internalValue = computed({
|
|
|
67
67
|
get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
|
|
68
68
|
set: (newValue: string) => {
|
|
69
69
|
if (modelValue.value !== undefined) {
|
|
70
|
-
modelValue.value = newValue
|
|
70
|
+
modelValue.value = newValue as string
|
|
71
71
|
}
|
|
72
72
|
emit('update:value', newValue)
|
|
73
73
|
},
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import Personnummer from 'personnummer'
|
|
2
|
+
import { normalizePidSearchValue } from '../composables/useIsPid'
|
|
3
|
+
|
|
4
|
+
export interface SsnValidationOptions {
|
|
5
|
+
allowCoordinationNumber?: boolean
|
|
6
|
+
allowInterimNumber?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SsnValidationResult {
|
|
10
|
+
isValid: boolean
|
|
11
|
+
/** Normalized value without separators; `null` when empty or invalid. */
|
|
12
|
+
digits: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const INVALID_RESULT: SsnValidationResult = {
|
|
16
|
+
isValid: false,
|
|
17
|
+
digits: null,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SSN_VALIDATION_OPTIONS = {
|
|
21
|
+
allowCoordinationNumber: true,
|
|
22
|
+
allowInterimNumber: true,
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
function resolveSsnValidationOptions(options?: SsnValidationOptions) {
|
|
26
|
+
return {
|
|
27
|
+
...DEFAULT_SSN_VALIDATION_OPTIONS,
|
|
28
|
+
...options,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Strip separators; keeps alphanumeric characters for interim numbers. */
|
|
33
|
+
export function stripSsnSeparators(value: string): string {
|
|
34
|
+
return normalizePidSearchValue(value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Validate a Swedish personal identity number. */
|
|
38
|
+
export function validateSsnNumber(value: string, options?: SsnValidationOptions): SsnValidationResult {
|
|
39
|
+
const trimmed = value.trim()
|
|
40
|
+
if (!trimmed) {
|
|
41
|
+
return INVALID_RESULT
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const resolvedOptions = resolveSsnValidationOptions(options)
|
|
45
|
+
const isValid = Personnummer.valid(trimmed, resolvedOptions)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
isValid,
|
|
49
|
+
digits: isValid ? stripSsnSeparators(trimmed) : null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateSsn(value: string, options?: SsnValidationOptions): boolean {
|
|
54
|
+
return validateSsnNumber(value, options).isValid
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `null` when empty, otherwise whether the value is a valid personnummer. */
|
|
58
|
+
export function getSsnValidationState(value: string, options?: SsnValidationOptions): boolean | null {
|
|
59
|
+
if (!value.trim()) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
return validateSsn(value, options)
|
|
63
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import FdsWizard from './components/FdsWizard/FdsWizard.vue'
|
|
|
31
31
|
import FdsCheckbox from './components/Form/FdsCheckbox/FdsCheckbox.vue'
|
|
32
32
|
import FdsInput from './components/Form/FdsInput/FdsInput.vue'
|
|
33
33
|
import FdsPhonenumber from './components/Form/FdsPhonenumber/FdsPhonenumber.vue'
|
|
34
|
+
import FdsSsn from './components/Form/FdsSsn/FdsSsn.vue'
|
|
34
35
|
import FdsRadio from './components/Form/FdsRadio/FdsRadio.vue'
|
|
35
36
|
import FdsSelect from './components/Form/FdsSelect/FdsSelect.vue'
|
|
36
37
|
import FdsTextarea from './components/Form/FdsTextarea/FdsTextarea.vue'
|
|
@@ -113,6 +114,7 @@ export {
|
|
|
113
114
|
FdsModal,
|
|
114
115
|
FdsPagination,
|
|
115
116
|
FdsPhonenumber,
|
|
117
|
+
FdsSsn,
|
|
116
118
|
FdsPopover,
|
|
117
119
|
FdsRadio,
|
|
118
120
|
FdsSearchSelect,
|
|
@@ -196,6 +198,7 @@ const FdsVueCorePlugin: Plugin = {
|
|
|
196
198
|
app.component('FdsCheckbox', FdsCheckbox)
|
|
197
199
|
app.component('FdsTextarea', FdsTextarea)
|
|
198
200
|
app.component('FdsPhonenumber', FdsPhonenumber)
|
|
201
|
+
app.component('FdsSsn', FdsSsn)
|
|
199
202
|
app.component('FdsSelect', FdsSelect)
|
|
200
203
|
app.component('FdsTable', FdsTable)
|
|
201
204
|
app.component('FdsTableHead', FdsTableHead)
|
|
@@ -283,6 +286,19 @@ export {
|
|
|
283
286
|
|
|
284
287
|
export type { FdsPhonenumberEmits, FdsPhonenumberProps } from './components/Form/FdsPhonenumber/types'
|
|
285
288
|
|
|
289
|
+
export {
|
|
290
|
+
getSsnValidationState,
|
|
291
|
+
stripSsnSeparators,
|
|
292
|
+
validateSsn,
|
|
293
|
+
validateSsnNumber,
|
|
294
|
+
type SsnValidationOptions,
|
|
295
|
+
type SsnValidationResult,
|
|
296
|
+
} from './helpers/validateSsn'
|
|
297
|
+
|
|
298
|
+
export { getSsnMask, getSsnMaskOptions, SSN_INTERIM_MASK, SSN_MASK } from './components/Form/FdsSsn/ssnMask'
|
|
299
|
+
|
|
300
|
+
export type { FdsSsnEmits, FdsSsnProps } from './components/Form/FdsSsn/types'
|
|
301
|
+
|
|
286
302
|
// Table component types
|
|
287
303
|
export type { FdsTableProps } from './components/Table/FdsTable/types'
|
|
288
304
|
|
package/src/lang/en.json
CHANGED
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
"FdsPhonenumber.invalidPhone": "Enter a valid phone number",
|
|
51
51
|
"FdsPhonenumber.noCountryResults": "No country matches your search",
|
|
52
52
|
"FdsPhonenumber.phoneNumber": "Phone number",
|
|
53
|
+
"FdsSsn.invalidSsn": "Enter a valid personal identity number",
|
|
54
|
+
"FdsSsn.label": "Personal identity number",
|
|
53
55
|
"FdsSearchSelectPro.loadingMore": "Loading more...",
|
|
54
56
|
"FdsSearchSelectPro.showMore": "Show more",
|
|
55
57
|
"FdsSearchSelectPro.unspecified": "Unspecified",
|
package/src/lang/sv.json
CHANGED
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
"FdsPhonenumber.invalidPhone": "Ange ett giltigt telefonnummer",
|
|
51
51
|
"FdsPhonenumber.noCountryResults": "Inget land matchar sökningen",
|
|
52
52
|
"FdsPhonenumber.phoneNumber": "Telefonnummer",
|
|
53
|
+
"FdsSsn.invalidSsn": "Ange ett giltigt personnummer",
|
|
54
|
+
"FdsSsn.label": "Personnummer",
|
|
53
55
|
"FdsSearchSelectPro.loadingMore": "Hämtar fler...",
|
|
54
56
|
"FdsSearchSelectPro.showMore": "Visa fler",
|
|
55
57
|
"FdsSearchSelectPro.unspecified": "Ospecificerat",
|