@naptics/vue-collection 0.2.14 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/.github/workflows/build.yml +26 -0
  2. package/.github/workflows/deploy-demo.yml +46 -0
  3. package/.github/workflows/deploy-lib.yml +65 -0
  4. package/.gitlab-ci.yml +57 -0
  5. package/.nvmrc +1 -0
  6. package/.prettierrc +8 -0
  7. package/.vscode/extensions.json +10 -0
  8. package/.vscode/launch.json +23 -0
  9. package/.vscode/settings.json +13 -0
  10. package/babel.config.json +3 -0
  11. package/components/NAlert.d.ts +1 -44
  12. package/components/NBadge.d.ts +1 -133
  13. package/components/NBreadcrub.d.ts +2 -106
  14. package/components/NBreadcrub.js +1 -1
  15. package/components/NButton.d.ts +2 -118
  16. package/components/NCheckbox.d.ts +1 -32
  17. package/components/NCheckboxLabel.d.ts +1 -45
  18. package/components/NCheckboxLabel.js +1 -1
  19. package/components/NCrudModal.d.ts +9 -221
  20. package/components/NCrudModal.js +1 -1
  21. package/components/NDialog.d.ts +1 -110
  22. package/components/NDialog.js +1 -1
  23. package/components/NDropdown.d.ts +1 -69
  24. package/components/NDropdown.js +1 -1
  25. package/components/NDropzone.d.ts +1 -115
  26. package/components/NDropzone.js +1 -1
  27. package/components/NForm.d.ts +1 -23
  28. package/components/NFormModal.d.ts +9 -127
  29. package/components/NIconButton.d.ts +3 -159
  30. package/components/NIconButton.js +1 -1
  31. package/components/NIconCircle.d.ts +1 -87
  32. package/components/NInput.d.ts +1 -164
  33. package/components/NInput.js +1 -1
  34. package/components/NInputPhone.d.ts +2 -114
  35. package/components/NInputPhone.js +1 -1
  36. package/components/NInputSelect.d.ts +2 -187
  37. package/components/NInputSelect.js +1 -1
  38. package/components/NInputSuggestion.d.ts +2 -155
  39. package/components/NInputSuggestion.js +1 -1
  40. package/components/NLink.d.ts +1 -70
  41. package/components/NList.d.ts +1 -43
  42. package/components/NList.js +1 -1
  43. package/components/NLoadingIndicator.d.ts +1 -49
  44. package/components/NModal.d.ts +15 -227
  45. package/components/NModal.js +16 -2
  46. package/components/NPagination.d.ts +1 -63
  47. package/components/NSearchbar.d.ts +1 -56
  48. package/components/NSearchbarList.d.ts +3 -63
  49. package/components/NSearchbarList.js +1 -1
  50. package/components/NSelect.d.ts +2 -148
  51. package/components/NSelect.js +1 -1
  52. package/components/NSuggestionList.d.ts +3 -126
  53. package/components/NSuggestionList.js +5 -2
  54. package/components/NTable.d.ts +1 -85
  55. package/components/NTable.js +12 -6
  56. package/components/NTableAction.d.ts +2 -46
  57. package/components/NTableAction.js +1 -1
  58. package/components/NTextArea.d.ts +2 -181
  59. package/components/NTextArea.js +1 -1
  60. package/components/NTooltip.d.ts +1 -105
  61. package/components/NTooltip.js +1 -1
  62. package/components/NValInput.d.ts +7 -182
  63. package/components/NValInput.js +1 -1
  64. package/env.d.ts +15 -0
  65. package/eslint.config.cjs +29 -0
  66. package/index.html +13 -0
  67. package/package.json +21 -19
  68. package/postcss.config.js +6 -0
  69. package/public/favicon.ico +0 -0
  70. package/scripts/build-lib.sh +52 -0
  71. package/scripts/sync-node-types.js +70 -0
  72. package/src/demo/App.css +9 -0
  73. package/src/demo/App.tsx +5 -0
  74. package/src/demo/components/ColorGrid.tsx +26 -0
  75. package/src/demo/components/ComponentGrid.tsx +26 -0
  76. package/src/demo/components/ComponentSection.tsx +30 -0
  77. package/src/demo/components/VariantSection.tsx +18 -0
  78. package/src/demo/i18n/de.ts +7 -0
  79. package/src/demo/i18n/en.ts +7 -0
  80. package/src/demo/i18n/index.ts +24 -0
  81. package/src/demo/main.ts +13 -0
  82. package/src/demo/router/index.ts +21 -0
  83. package/src/demo/views/HomeView.tsx +94 -0
  84. package/src/demo/views/NavigationView.tsx +43 -0
  85. package/src/demo/views/presentation/AlertView.tsx +40 -0
  86. package/src/demo/views/presentation/BadgeView.tsx +61 -0
  87. package/src/demo/views/presentation/BreadcrumbView.tsx +52 -0
  88. package/src/demo/views/presentation/ButtonView.tsx +49 -0
  89. package/src/demo/views/presentation/CheckboxView.tsx +59 -0
  90. package/src/demo/views/presentation/DropdownView.tsx +59 -0
  91. package/src/demo/views/presentation/DropzoneView.tsx +39 -0
  92. package/src/demo/views/presentation/IconButtonView.tsx +47 -0
  93. package/src/demo/views/presentation/IconCircleView.tsx +38 -0
  94. package/src/demo/views/presentation/InputView.tsx +179 -0
  95. package/src/demo/views/presentation/LinkView.tsx +50 -0
  96. package/src/demo/views/presentation/ListView.tsx +29 -0
  97. package/src/demo/views/presentation/LoadingIndicatorView.tsx +38 -0
  98. package/src/demo/views/presentation/ModalView.tsx +210 -0
  99. package/src/demo/views/presentation/PaginationView.tsx +25 -0
  100. package/src/demo/views/presentation/SearchbarView.tsx +80 -0
  101. package/src/demo/views/presentation/TableView.tsx +146 -0
  102. package/src/demo/views/presentation/TooltipView.tsx +86 -0
  103. package/src/lib/components/NAlert.tsx +85 -0
  104. package/src/lib/components/NBadge.tsx +75 -0
  105. package/src/lib/components/NBreadcrub.tsx +97 -0
  106. package/src/lib/components/NButton.tsx +80 -0
  107. package/src/lib/components/NCheckbox.tsx +55 -0
  108. package/src/lib/components/NCheckboxLabel.tsx +51 -0
  109. package/src/lib/components/NCrudModal.tsx +133 -0
  110. package/src/lib/components/NDialog.tsx +182 -0
  111. package/src/lib/components/NDropdown.tsx +167 -0
  112. package/src/lib/components/NDropzone.tsx +265 -0
  113. package/src/lib/components/NForm.tsx +32 -0
  114. package/src/lib/components/NFormModal.tsx +66 -0
  115. package/src/lib/components/NIconButton.tsx +92 -0
  116. package/src/lib/components/NIconCircle.tsx +78 -0
  117. package/src/lib/components/NInput.css +11 -0
  118. package/src/lib/components/NInput.tsx +139 -0
  119. package/src/lib/components/NInputPhone.tsx +53 -0
  120. package/src/lib/components/NInputSelect.tsx +126 -0
  121. package/src/lib/components/NInputSuggestion.tsx +80 -0
  122. package/src/lib/components/NLink.tsx +68 -0
  123. package/src/lib/components/NList.tsx +67 -0
  124. package/src/lib/components/NLoadingIndicator.css +46 -0
  125. package/src/lib/components/NLoadingIndicator.tsx +63 -0
  126. package/src/lib/components/NModal.tsx +243 -0
  127. package/src/lib/components/NPagination.css +15 -0
  128. package/src/lib/components/NPagination.tsx +131 -0
  129. package/src/lib/components/NSearchbar.tsx +78 -0
  130. package/src/lib/components/NSearchbarList.tsx +47 -0
  131. package/src/lib/components/NSelect.tsx +128 -0
  132. package/src/lib/components/NSuggestionList.tsx +216 -0
  133. package/src/lib/components/NTable.css +3 -0
  134. package/src/lib/components/NTable.tsx +247 -0
  135. package/src/lib/components/NTableAction.tsx +49 -0
  136. package/src/lib/components/NTextArea.tsx +159 -0
  137. package/src/lib/components/NTooltip.css +37 -0
  138. package/src/lib/components/NTooltip.tsx +250 -0
  139. package/src/lib/components/NValInput.tsx +163 -0
  140. package/src/lib/components/ValidatedForm.ts +71 -0
  141. package/src/lib/components/__tests__/NButton.spec.tsx +26 -0
  142. package/src/lib/components/__tests__/NCheckbox.spec.tsx +39 -0
  143. package/src/lib/i18n/de/vue-collection.json +58 -0
  144. package/src/lib/i18n/en/vue-collection.json +58 -0
  145. package/src/lib/i18n/index.ts +54 -0
  146. package/src/lib/index.ts +2 -0
  147. package/src/lib/jsx.d.ts +13 -0
  148. package/src/lib/utils/__tests__/identifiable.spec.ts +72 -0
  149. package/src/lib/utils/__tests__/validation.spec.ts +92 -0
  150. package/src/lib/utils/breakpoints.ts +47 -0
  151. package/src/lib/utils/component.tsx +131 -0
  152. package/src/lib/utils/deferred.ts +28 -0
  153. package/src/lib/utils/identifiable.ts +87 -0
  154. package/src/lib/utils/stringMaxLength.ts +25 -0
  155. package/src/lib/utils/tailwind.ts +41 -0
  156. package/src/lib/utils/utils.ts +90 -0
  157. package/src/lib/utils/vModel.ts +260 -0
  158. package/src/lib/utils/validation.ts +189 -0
  159. package/src/lib/utils/vue.ts +25 -0
  160. package/tailwind.config.js +38 -0
  161. package/tsconfig.config.json +9 -0
  162. package/tsconfig.demo.json +19 -0
  163. package/tsconfig.json +16 -0
  164. package/tsconfig.lib.json +18 -0
  165. package/tsconfig.vitest.json +8 -0
  166. package/utils/breakpoints.d.ts +1 -1
  167. package/utils/component.d.ts +3 -7
  168. package/utils/component.js +5 -2
  169. package/utils/identifiable.js +5 -1
  170. package/vite.config.ts +28 -0
@@ -0,0 +1,66 @@
1
+ import { createComponentWithSlots } from '../utils/component'
2
+ import type { TWMaxWidth } from '../utils/tailwind'
3
+ import { type PropType, computed } from 'vue'
4
+ import NForm from './NForm'
5
+ import NModal, { nModalProps } from './NModal'
6
+ import type { ValidatedForm } from './ValidatedForm'
7
+
8
+ export const nFormModalProps = {
9
+ ...nModalProps,
10
+ /**
11
+ * The maximum width of the modal. A regular tailwind class.
12
+ */
13
+ maxWidth: {
14
+ type: String as PropType<TWMaxWidth>,
15
+ default: 'max-w-lg',
16
+ },
17
+ /**
18
+ * If set to `true` the modal closes when clicking on the background.
19
+ * Default is `false` as the accidental reseting of the whole form is very annoying.
20
+ */
21
+ closeOnBackground: {
22
+ type: Boolean,
23
+ default: false,
24
+ },
25
+ /**
26
+ * The {@link ValidatedForm} to validate the inputs.
27
+ * All inputs should be added to the form.
28
+ */
29
+ form: Object as PropType<ValidatedForm>,
30
+ } as const
31
+
32
+ /**
33
+ * The `NFormModal` is a {@link NModal} with an integrated form.
34
+ * When submitting a `NFormModal` the form is first validated and
35
+ * only if the validation is succesful the `onOk` event is called.
36
+ */
37
+ const Component = createComponentWithSlots(
38
+ 'NFormModal',
39
+ nFormModalProps,
40
+ ['modal', 'header', 'footer'],
41
+ (props, { slots }) => {
42
+ const onOk = () => {
43
+ if (!props.form || props.form.validate().isValid) {
44
+ props.onOk?.()
45
+ if (props.closeOnOk) props.onUpdateValue?.(false)
46
+ }
47
+ }
48
+
49
+ const childProps = computed(() => ({
50
+ ...props,
51
+ onOk,
52
+ closeOnOk: false,
53
+ onKeydown: (event: KeyboardEvent) => {
54
+ if (event.metaKey && event.key === 'Enter') onOk()
55
+ },
56
+ }))
57
+
58
+ return () => (
59
+ <NModal {...childProps.value}>
60
+ <NForm form={props.form}>{slots.default?.()}</NForm>
61
+ </NModal>
62
+ )
63
+ }
64
+ )
65
+
66
+ export { Component as NFormModal, Component as default }
@@ -0,0 +1,92 @@
1
+ import type { HeroIcon } from '../utils/tailwind'
2
+ import { createComponent } from '../utils/component'
3
+ import type { PropType } from 'vue'
4
+ import { RouterLink, type RouteLocationRaw } from 'vue-router'
5
+ import { nButtonProps } from './NButton'
6
+ import NTooltip, { mapTooltipProps, nToolTipPropsForImplementor } from './NTooltip'
7
+
8
+ export const nIconButtonProps = {
9
+ /**
10
+ * The icon of the icon-button.
11
+ */
12
+ icon: {
13
+ type: Function as PropType<HeroIcon>,
14
+ required: true,
15
+ },
16
+ /**
17
+ * The route of the icon-button. If this is set, the icon-button becomes a {@link RouterLink}.
18
+ */
19
+ route: [String, Object] as PropType<RouteLocationRaw>,
20
+ /**
21
+ * The color of the icon-button.
22
+ */
23
+ color: {
24
+ type: String,
25
+ default: 'default',
26
+ },
27
+ /**
28
+ * The shade of the icon-button.
29
+ */
30
+ shade: {
31
+ type: Number,
32
+ default: 500,
33
+ },
34
+ /**
35
+ * The size of the icon in tailwind-units.
36
+ */
37
+ size: {
38
+ type: Number,
39
+ default: 5,
40
+ },
41
+ /**
42
+ * The html attribute, which indicates the type of the button.
43
+ */
44
+ type: nButtonProps.type,
45
+ /**
46
+ * If set to `true` the icon-button is disabled and no interaction is possible.
47
+ */
48
+ disabled: Boolean,
49
+ /**
50
+ * Adds the classes to the icon-button.
51
+ * Use this instead of `class` to style the button, because the button is wrapped inside
52
+ * a div for the tooltip and `class` would be applied to the wrapping div.
53
+ */
54
+ buttonClass: String,
55
+ /**
56
+ * This is called when the icon-button is clicked.
57
+ * It is only called when the `route` prop is not set on the icon-button.
58
+ */
59
+ onClick: Function as PropType<() => void>,
60
+ ...nToolTipPropsForImplementor,
61
+ } as const
62
+
63
+ /**
64
+ * The `NIconButton` is a regular button which does not have any text but an icon instead.
65
+ */
66
+ const Component = createComponent('NIconButton', nIconButtonProps, props => {
67
+ const classes = () => [
68
+ 'block p-0.5 rounded-md focus:outline-none focus-visible:ring-2 -m-1',
69
+ props.disabled
70
+ ? `text-${props.color}-200 cursor-default`
71
+ : `hover:bg-${props.color}-${props.shade} hover:bg-opacity-10 text-${props.color}-${props.shade} focus-visible:ring-${props.color}-${props.shade} cursor-pointer`,
72
+ props.buttonClass,
73
+ ]
74
+
75
+ const content = () => <props.icon class={`w-${props.size} h-${props.size}`} />
76
+
77
+ return () => (
78
+ <NTooltip {...mapTooltipProps(props)}>
79
+ {props.route ? (
80
+ <RouterLink to={props.route} class={classes()}>
81
+ {content()}
82
+ </RouterLink>
83
+ ) : (
84
+ <button type={props.type} disabled={props.disabled} class={classes()} onClick={props.onClick}>
85
+ {content()}
86
+ </button>
87
+ )}
88
+ </NTooltip>
89
+ )
90
+ })
91
+
92
+ export { Component as NIconButton, Component as default }
@@ -0,0 +1,78 @@
1
+ import type { HeroIcon } from '../utils/tailwind'
2
+ import { createComponent } from '../utils/component'
3
+ import type { PropType } from 'vue'
4
+
5
+ export const nIconCircleProps = {
6
+ /**
7
+ * The icon of the icon-circle.
8
+ */
9
+ icon: {
10
+ type: Function as PropType<HeroIcon>,
11
+ required: true,
12
+ },
13
+ /**
14
+ * The color of the icon-circle.
15
+ */
16
+ color: {
17
+ type: String,
18
+ default: 'primary',
19
+ },
20
+ /**
21
+ * The size of the circle in "tailwind units" (4 px).
22
+ * Tailwind classes are used for the size, so any number can be passed.
23
+ * If the `iconSize` is not set, it will be adjusted automatically.
24
+ */
25
+ circleSize: Number,
26
+ /**
27
+ * The size of the icon in "tailwind units" (4 px).
28
+ * No tailwind classes are used for the size, so any number can be passed.
29
+ * If the `circleSize` is not set, it will be adjusted automatically.
30
+ */
31
+ iconSize: Number,
32
+ /**
33
+ * The shade of the icon.
34
+ */
35
+ iconShade: {
36
+ type: Number,
37
+ default: 600,
38
+ },
39
+ /**
40
+ * The shade of the background.
41
+ */
42
+ bgShade: {
43
+ type: Number,
44
+ default: 100,
45
+ },
46
+ } as const
47
+
48
+ const DEFAULT_CIRCLE_SIZE = 16
49
+ const SCALING_FACTOR = 0.55
50
+
51
+ /**
52
+ * The `NIconCircle` is an icon with a colored circle around it.
53
+ */
54
+ const Component = createComponent('NIconCircle', nIconCircleProps, props => {
55
+ let circleSize = props.circleSize
56
+ let iconSize = props.iconSize
57
+ if (circleSize == null) {
58
+ if (iconSize == null) circleSize = DEFAULT_CIRCLE_SIZE
59
+ else circleSize = iconSize / SCALING_FACTOR
60
+ }
61
+ if (iconSize == null) iconSize = circleSize * SCALING_FACTOR
62
+
63
+ circleSize *= 4
64
+ iconSize *= 4
65
+
66
+ return () => (
67
+ <div
68
+ class={['flex items-center justify-center rounded-full', `bg-${props.color}-${props.bgShade}`]}
69
+ style={`width: ${circleSize}px; height: ${circleSize}px`}
70
+ >
71
+ <div class={`text-${props.color}-${props.iconShade}`} style={`width: ${iconSize}px; height: ${iconSize}px`}>
72
+ <props.icon />
73
+ </div>
74
+ </div>
75
+ )
76
+ })
77
+
78
+ export { Component as NIconCircle, Component as default }
@@ -0,0 +1,11 @@
1
+ /* Chrome, Safari, Edge, Opera */
2
+ input::-webkit-outer-spin-button,
3
+ input::-webkit-inner-spin-button {
4
+ -webkit-appearance: none;
5
+ margin: 0;
6
+ }
7
+
8
+ /* Firefox */
9
+ input[type='number'] {
10
+ -moz-appearance: textfield;
11
+ }
@@ -0,0 +1,139 @@
1
+ import { createComponent } from '../utils/component'
2
+ import { ref, type PropType } from 'vue'
3
+ import { ExclamationCircleIcon } from '@heroicons/vue/24/solid'
4
+ import NTooltip, { mapTooltipProps, nToolTipPropsForImplementor } from './NTooltip'
5
+ import './NInput.css'
6
+ import { vModelProps } from '../utils/vModel'
7
+
8
+ export const nInputProps = {
9
+ ...vModelProps(String),
10
+ /**
11
+ * The name of the input. Is displayed as a label above the input.
12
+ */
13
+ name: String,
14
+ /**
15
+ * The placeholder of the input.
16
+ */
17
+ placeholder: String,
18
+ /**
19
+ * The html autocomplete attribute of the input.
20
+ */
21
+ autocomplete: {
22
+ type: String,
23
+ default: 'off',
24
+ },
25
+ /**
26
+ * The html type attribute of the input.
27
+ */
28
+ type: {
29
+ type: String,
30
+ default: 'text',
31
+ },
32
+ /**
33
+ * The maximum value of the input.
34
+ */
35
+ max: String,
36
+ /**
37
+ * The minimum value of the input.
38
+ */
39
+ min: String,
40
+ /**
41
+ * If set to `true` the input is displayed with a red border.
42
+ */
43
+ error: Boolean,
44
+ /**
45
+ * If set to `true` the input is disabled and no interaction is possible.
46
+ */
47
+ disabled: Boolean,
48
+ /**
49
+ * If set to `true` the input is displayed smaller.
50
+ */
51
+ small: Boolean,
52
+ /**
53
+ * If set to `true` the input's label is hidden.
54
+ */
55
+ hideLabel: Boolean,
56
+ /**
57
+ * Adds the classes directly to the input (e.g. for shadow).
58
+ */
59
+ inputClass: String,
60
+ /**
61
+ * This is called when the input reveices focus.
62
+ */
63
+ onFocus: Function as PropType<() => void>,
64
+ /**
65
+ * This is called when the input looses focus.
66
+ */
67
+ onBlur: Function as PropType<() => void>,
68
+ ...nToolTipPropsForImplementor,
69
+ } as const
70
+
71
+ export type NInputExposed = {
72
+ /**
73
+ * Request focus on the input.
74
+ */
75
+ focus(): void
76
+ }
77
+
78
+ /**
79
+ * The base class of inputs. A styled input with a lot of configuration possibilities but no validation.
80
+ */
81
+ const Component = createComponent('NInput', nInputProps, (props, context) => {
82
+ const inputRef = ref<HTMLInputElement>()
83
+ const exposed: NInputExposed = {
84
+ focus: () => inputRef.value?.focus(),
85
+ }
86
+ context.expose(exposed)
87
+
88
+ return () => (
89
+ <div>
90
+ {props.name && !props.hideLabel && (
91
+ <label
92
+ for={props.name}
93
+ class={['block text-sm font-medium mb-1', props.disabled ? 'text-default-300' : 'text-default-700']}
94
+ >
95
+ {props.name}
96
+ </label>
97
+ )}
98
+ <NTooltip block {...mapTooltipProps(props)}>
99
+ <div class="relative">
100
+ <input
101
+ ref={inputRef}
102
+ name={props.name}
103
+ value={props.value}
104
+ onInput={event => props.onUpdateValue?.((event.target as HTMLInputElement).value)}
105
+ placeholder={props.placeholder}
106
+ autocomplete={props.autocomplete}
107
+ type={props.type}
108
+ min={props.min}
109
+ max={props.max}
110
+ disabled={props.disabled}
111
+ onFocus={() => props.onFocus?.()}
112
+ onBlur={() => props.onBlur?.()}
113
+ onInvalid={event => event.preventDefault()}
114
+ class={[
115
+ 'block w-full rounded-md border focus:outline-none focus:ring-1 ',
116
+ props.small ? 'text-xs py-0.5 px-2' : 'py-2 px-4',
117
+ props.disabled
118
+ ? 'text-default-500 placeholder-default-300 bg-default-50'
119
+ : 'text-default-900 placeholder-default-400 ',
120
+ props.error
121
+ ? 'border-red-500 focus:border-red-500 focus:ring-red-500 pr-10'
122
+ : 'border-default-300 focus:border-primary-500 focus:ring-primary-500',
123
+ props.inputClass,
124
+ ]}
125
+ />
126
+
127
+ <div
128
+ class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"
129
+ v-show={props.error && !props.small}
130
+ >
131
+ <ExclamationCircleIcon class="h-5 w-5 text-red-700" aria-hidden="true" />
132
+ </div>
133
+ </div>
134
+ </NTooltip>
135
+ </div>
136
+ )
137
+ })
138
+
139
+ export { Component as NInput, Component as default }
@@ -0,0 +1,53 @@
1
+ import { createComponent } from '../utils/component'
2
+ import { external } from '../utils/validation'
3
+ import { computed, Suspense } from 'vue'
4
+ import NValInput, { nValInputProps } from './NValInput'
5
+ import { trsl } from '../i18n'
6
+
7
+ export const nInputPhoneProps = nValInputProps
8
+
9
+ /**
10
+ * The `NInputPhone` autoformats phone numbers and checks if they are valid.
11
+ */
12
+ const Component = createComponent('NInputPhoneSuspended', nInputPhoneProps, props => {
13
+ // Async components have to be wrapped in a suspense component.
14
+ return () => (
15
+ <Suspense>
16
+ <NPhoneInput {...props} />
17
+ </Suspense>
18
+ )
19
+ })
20
+
21
+ export { Component as NInputPhone, Component as default }
22
+
23
+ const NPhoneInput = createComponent('NInputPhone', nInputPhoneProps, async props => {
24
+ // import dynamically for better codesplitting as the library is pretty large
25
+ const { parsePhoneNumber } = await import('awesome-phonenumber')
26
+ const DEFAULT_COUNTRY_CODE = 'CH'
27
+
28
+ const formattedToPlain = (number: string) =>
29
+ parsePhoneNumber(number, { regionCode: DEFAULT_COUNTRY_CODE }).number?.e164
30
+ const plainToFormatted = (number: string) =>
31
+ parsePhoneNumber(number, { regionCode: DEFAULT_COUNTRY_CODE }).number?.international
32
+
33
+ const onUpdateValue = (newValue: string) => {
34
+ const plain = formattedToPlain(newValue)
35
+ props.onUpdateValue?.(plain || newValue)
36
+ }
37
+
38
+ const value = computed(() => {
39
+ const formatted = plainToFormatted(props.value || '')
40
+ return formatted || props.value
41
+ })
42
+
43
+ const isValid = computed(() => parsePhoneNumber(props.value || '').valid)
44
+
45
+ return () => (
46
+ <NValInput
47
+ {...{ ...props, onUpdateValue }}
48
+ value={value.value}
49
+ rules={external(isValid.value, trsl('vue-collection.validation.rules.phone'))}
50
+ type="tel"
51
+ />
52
+ )
53
+ })
@@ -0,0 +1,126 @@
1
+ import { createComponentWithSlots } from '../utils/component'
2
+ import { Id, type Identifiable } from '../utils/identifiable'
3
+ import { option } from '../utils/validation'
4
+ import { vModelForRef } from '../utils/vModel'
5
+ import { watchRef } from '../utils/vue'
6
+ import { computed, ref, watch, type PropType } from 'vue'
7
+ import { nInputProps } from './NInput'
8
+ import NSuggestionList, { nSuggestionListProps } from './NSuggestionList'
9
+ import NValInput, { nValInputProps, type NValInputExposed } from './NValInput'
10
+ import type { AnyObject } from '../utils/utils'
11
+
12
+ export const nInputSelectProps = {
13
+ ...nInputProps,
14
+ /**
15
+ * The id of the currently selected option of this input.
16
+ */
17
+ value: String,
18
+ /**
19
+ * This is called with the newly selected id when the selection has changed.
20
+ * This happens, when an item from the suggestion list is selected or the
21
+ * input matches a selection option exactly.
22
+ * If no id is selected, the empty string is passed, in order to
23
+ * match the API of all other inputs who never pass `undefined`.
24
+ */
25
+ onUpdateValue: Function as PropType<(newValue: string) => void>,
26
+ /**
27
+ * The options which are allowed and suggested for this input.
28
+ * The options are filtered based on the user input.
29
+ */
30
+ options: {
31
+ type: Array as PropType<InputSelectOption[]>,
32
+ default: () => [],
33
+ },
34
+ /**
35
+ * @see {@link nValInputProps.optional}
36
+ */
37
+ optional: nValInputProps.optional,
38
+ /**
39
+ * @see {@link nValInputProps.form}
40
+ */
41
+ form: nValInputProps.form,
42
+ /**
43
+ * @see {@link nValInputProps.error}
44
+ */
45
+ error: nValInputProps.error,
46
+ /**
47
+ * @see {@link nValInputProps.errorMessage}
48
+ */
49
+ errorMessage: nValInputProps.errorMessage,
50
+ /**
51
+ * If set to `true` the list is hidden even if there are still matching items in the list.
52
+ */
53
+ hideList: nSuggestionListProps.hideList,
54
+ /**
55
+ * @see {@link nSuggestionListProps.maxItems}
56
+ */
57
+ maxItems: nSuggestionListProps.maxItems,
58
+ /**
59
+ * @see {@link nSuggestionListProps.listItem}
60
+ */
61
+ listItem: nSuggestionListProps.listItem,
62
+ } as const
63
+
64
+ export type InputSelectOption = Identifiable & { label: string } & AnyObject
65
+
66
+ /**
67
+ * The `NInputSelect` is very similar to the {@link NSelect}, but instead of a select input it is a regular input.
68
+ * The user is forced to use a value from the specified options of the input.
69
+ * While they type, the list of options is shown to them and filtered based on their input.
70
+ */
71
+ const Component = createComponentWithSlots('NInputSelect', nInputSelectProps, ['listItem'], props => {
72
+ const inputRef = ref<NValInputExposed>()
73
+
74
+ const inputValue = ref('')
75
+ watch(
76
+ () => props.value,
77
+ newValue => {
78
+ if (newValue) {
79
+ const chosenOption = Id.find(props.options, newValue)
80
+ if (chosenOption) inputValue.value = chosenOption.label
81
+ }
82
+ },
83
+ { immediate: true }
84
+ )
85
+
86
+ const filteredOptions = computed(() =>
87
+ props.options.filter(option => option.label.includes(inputValue.value || ''))
88
+ )
89
+
90
+ const matchedOption = computed(() => {
91
+ const matches = props.options.filter(option => option.label === inputValue.value)
92
+ return matches.length > 0 ? matches[0] : null
93
+ })
94
+ watchRef(matchedOption, newOption => props.onUpdateValue?.(newOption?.id || ''))
95
+
96
+ return () => (
97
+ <NSuggestionList
98
+ items={filteredOptions.value}
99
+ onSelect={props.onUpdateValue}
100
+ inputValue={inputValue.value}
101
+ hideList={props.hideList || matchedOption.value != null || filteredOptions.value.length == 0}
102
+ maxItems={props.maxItems}
103
+ listItem={props.listItem}
104
+ input={({ onFocus, onBlur }) => (
105
+ <NValInput
106
+ ref={inputRef}
107
+ {...{ ...props, ...vModelForRef(inputValue) }}
108
+ rules={option(props.options.map(opt => opt.label))}
109
+ onFocus={() => {
110
+ onFocus()
111
+ props.onFocus?.()
112
+ }}
113
+ onBlur={onBlur}
114
+ disableBlurValidation
115
+ />
116
+ )}
117
+ onRequestInputFocus={() => inputRef.value?.focus()}
118
+ onRealBlur={() => {
119
+ props.onBlur?.()
120
+ inputRef?.value?.validate()
121
+ }}
122
+ />
123
+ )
124
+ })
125
+
126
+ export { Component as NInputSelect, Component as default }
@@ -0,0 +1,80 @@
1
+ import { createComponent } from '../utils/component'
2
+ import { Id } from '../utils/identifiable'
3
+ import { computed, ref, type PropType } from 'vue'
4
+ import NSuggestionList, { nSuggestionListProps } from './NSuggestionList'
5
+ import NValInput, { nValInputProps, type NValInputExposed } from './NValInput'
6
+
7
+ export const nInputSuggestionProps = {
8
+ ...nValInputProps,
9
+ /**
10
+ * If set to `true` the list is hidden even if there are still matching items in the list.
11
+ */
12
+ hideList: nSuggestionListProps.hideList,
13
+ /**
14
+ * @see {@link nSuggestionListProps.maxItems}
15
+ */
16
+ maxItems: nSuggestionListProps.maxItems,
17
+ /**
18
+ * The suggestions which are shown to the user for this input.
19
+ * The suggestions are filtered based on the user input.
20
+ */
21
+ suggestions: {
22
+ type: Array as PropType<string[]>,
23
+ default: () => [],
24
+ },
25
+ } as const
26
+
27
+ /**
28
+ * `NInputSuggestion` is an input, which shows a list of possible suggestions to the user
29
+ * which is filtered while typing. Contrary to {@link NInputSelect} the user is not required to choose any of the suggestions.
30
+ */
31
+ const Component = createComponent('NInputSuggestion', nInputSuggestionProps, props => {
32
+ const suggestionItems = computed(() =>
33
+ props.suggestions
34
+ .filter(suggestion => suggestion.includes(props.value || ''))
35
+ .map((value, index) => ({
36
+ id: index.toString(),
37
+ label: value,
38
+ }))
39
+ )
40
+
41
+ const select = (id: string) => props.onUpdateValue?.(Id.find(suggestionItems.value, id)?.label || '')
42
+
43
+ const hideList = computed(
44
+ () =>
45
+ props.hideList ||
46
+ suggestionItems.value.length == 0 ||
47
+ suggestionItems.value.filter(suggestion => suggestion.label !== props.value).length == 0
48
+ )
49
+
50
+ const inputRef = ref<NValInputExposed>()
51
+
52
+ return () => (
53
+ <NSuggestionList
54
+ items={suggestionItems.value}
55
+ onSelect={id => select(id)}
56
+ inputValue={props.value || ''}
57
+ hideList={hideList.value}
58
+ maxItems={props.maxItems}
59
+ input={({ onFocus, onBlur }) => (
60
+ <NValInput
61
+ ref={inputRef}
62
+ {...props}
63
+ onFocus={() => {
64
+ onFocus()
65
+ props.onFocus?.()
66
+ }}
67
+ onBlur={onBlur}
68
+ disableBlurValidation
69
+ />
70
+ )}
71
+ onRequestInputFocus={() => inputRef.value?.focus()}
72
+ onRealBlur={() => {
73
+ props.onBlur?.()
74
+ inputRef?.value?.validate()
75
+ }}
76
+ />
77
+ )
78
+ })
79
+
80
+ export { Component as NInputSuggestion, Component as default }