@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.
Files changed (171) 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 +59 -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 +7 -251
  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 +7 -151
  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 +6 -70
  41. package/components/NLink.js +8 -1
  42. package/components/NList.d.ts +1 -43
  43. package/components/NList.js +1 -1
  44. package/components/NLoadingIndicator.d.ts +1 -49
  45. package/components/NModal.d.ts +12 -250
  46. package/components/NModal.js +15 -9
  47. package/components/NPagination.d.ts +1 -63
  48. package/components/NSearchbar.d.ts +1 -56
  49. package/components/NSearchbarList.d.ts +3 -63
  50. package/components/NSearchbarList.js +1 -1
  51. package/components/NSelect.d.ts +2 -148
  52. package/components/NSelect.js +1 -1
  53. package/components/NSuggestionList.d.ts +3 -126
  54. package/components/NSuggestionList.js +5 -2
  55. package/components/NTable.d.ts +1 -85
  56. package/components/NTable.js +12 -6
  57. package/components/NTableAction.d.ts +2 -46
  58. package/components/NTableAction.js +1 -1
  59. package/components/NTextArea.d.ts +2 -181
  60. package/components/NTextArea.js +1 -1
  61. package/components/NTooltip.d.ts +1 -105
  62. package/components/NTooltip.js +1 -1
  63. package/components/NValInput.d.ts +7 -182
  64. package/components/NValInput.js +1 -1
  65. package/env.d.ts +15 -0
  66. package/eslint.config.cjs +29 -0
  67. package/index.html +13 -0
  68. package/package.json +21 -19
  69. package/postcss.config.js +6 -0
  70. package/public/favicon.ico +0 -0
  71. package/scripts/build-lib.sh +52 -0
  72. package/scripts/sync-node-types.js +70 -0
  73. package/src/demo/App.css +9 -0
  74. package/src/demo/App.tsx +5 -0
  75. package/src/demo/components/ColorGrid.tsx +26 -0
  76. package/src/demo/components/ComponentGrid.tsx +26 -0
  77. package/src/demo/components/ComponentSection.tsx +30 -0
  78. package/src/demo/components/VariantSection.tsx +18 -0
  79. package/src/demo/i18n/de.ts +7 -0
  80. package/src/demo/i18n/en.ts +7 -0
  81. package/src/demo/i18n/index.ts +24 -0
  82. package/src/demo/main.ts +13 -0
  83. package/src/demo/router/index.ts +21 -0
  84. package/src/demo/views/HomeView.tsx +94 -0
  85. package/src/demo/views/NavigationView.tsx +43 -0
  86. package/src/demo/views/presentation/AlertView.tsx +40 -0
  87. package/src/demo/views/presentation/BadgeView.tsx +61 -0
  88. package/src/demo/views/presentation/BreadcrumbView.tsx +52 -0
  89. package/src/demo/views/presentation/ButtonView.tsx +49 -0
  90. package/src/demo/views/presentation/CheckboxView.tsx +59 -0
  91. package/src/demo/views/presentation/DropdownView.tsx +59 -0
  92. package/src/demo/views/presentation/DropzoneView.tsx +39 -0
  93. package/src/demo/views/presentation/IconButtonView.tsx +47 -0
  94. package/src/demo/views/presentation/IconCircleView.tsx +38 -0
  95. package/src/demo/views/presentation/InputView.tsx +179 -0
  96. package/src/demo/views/presentation/LinkView.tsx +60 -0
  97. package/src/demo/views/presentation/ListView.tsx +29 -0
  98. package/src/demo/views/presentation/LoadingIndicatorView.tsx +38 -0
  99. package/src/demo/views/presentation/ModalView.tsx +210 -0
  100. package/src/demo/views/presentation/PaginationView.tsx +25 -0
  101. package/src/demo/views/presentation/SearchbarView.tsx +80 -0
  102. package/src/demo/views/presentation/TableView.tsx +146 -0
  103. package/src/demo/views/presentation/TooltipView.tsx +86 -0
  104. package/src/lib/components/NAlert.tsx +85 -0
  105. package/src/lib/components/NBadge.tsx +75 -0
  106. package/src/lib/components/NBreadcrub.tsx +97 -0
  107. package/src/lib/components/NButton.tsx +80 -0
  108. package/src/lib/components/NCheckbox.tsx +55 -0
  109. package/src/lib/components/NCheckboxLabel.tsx +51 -0
  110. package/src/lib/components/NCrudModal.tsx +133 -0
  111. package/src/lib/components/NDialog.tsx +182 -0
  112. package/src/lib/components/NDropdown.tsx +167 -0
  113. package/src/lib/components/NDropzone.tsx +265 -0
  114. package/src/lib/components/NForm.tsx +32 -0
  115. package/src/lib/components/NFormModal.tsx +66 -0
  116. package/src/lib/components/NIconButton.tsx +92 -0
  117. package/src/lib/components/NIconCircle.tsx +78 -0
  118. package/src/lib/components/NInput.css +11 -0
  119. package/src/lib/components/NInput.tsx +139 -0
  120. package/src/lib/components/NInputPhone.tsx +53 -0
  121. package/src/lib/components/NInputSelect.tsx +126 -0
  122. package/src/lib/components/NInputSuggestion.tsx +80 -0
  123. package/src/lib/components/NLink.tsx +82 -0
  124. package/src/lib/components/NList.tsx +67 -0
  125. package/src/lib/components/NLoadingIndicator.css +46 -0
  126. package/src/lib/components/NLoadingIndicator.tsx +63 -0
  127. package/src/lib/components/NModal.tsx +243 -0
  128. package/src/lib/components/NPagination.css +15 -0
  129. package/src/lib/components/NPagination.tsx +131 -0
  130. package/src/lib/components/NSearchbar.tsx +78 -0
  131. package/src/lib/components/NSearchbarList.tsx +47 -0
  132. package/src/lib/components/NSelect.tsx +128 -0
  133. package/src/lib/components/NSuggestionList.tsx +216 -0
  134. package/src/lib/components/NTable.css +3 -0
  135. package/src/lib/components/NTable.tsx +247 -0
  136. package/src/lib/components/NTableAction.tsx +49 -0
  137. package/src/lib/components/NTextArea.tsx +159 -0
  138. package/src/lib/components/NTooltip.css +37 -0
  139. package/src/lib/components/NTooltip.tsx +250 -0
  140. package/src/lib/components/NValInput.tsx +163 -0
  141. package/src/lib/components/ValidatedForm.ts +71 -0
  142. package/src/lib/components/__tests__/NButton.spec.tsx +26 -0
  143. package/src/lib/components/__tests__/NCheckbox.spec.tsx +39 -0
  144. package/src/lib/i18n/de/vue-collection.json +58 -0
  145. package/src/lib/i18n/en/vue-collection.json +58 -0
  146. package/src/lib/i18n/index.ts +54 -0
  147. package/src/lib/index.ts +2 -0
  148. package/src/lib/jsx.d.ts +13 -0
  149. package/src/lib/utils/__tests__/identifiable.spec.ts +72 -0
  150. package/src/lib/utils/__tests__/validation.spec.ts +92 -0
  151. package/src/lib/utils/breakpoints.ts +47 -0
  152. package/src/lib/utils/component.tsx +131 -0
  153. package/src/lib/utils/deferred.ts +28 -0
  154. package/src/lib/utils/identifiable.ts +87 -0
  155. package/src/lib/utils/stringMaxLength.ts +25 -0
  156. package/src/lib/utils/tailwind.ts +41 -0
  157. package/src/lib/utils/utils.ts +90 -0
  158. package/src/lib/utils/vModel.ts +260 -0
  159. package/src/lib/utils/validation.ts +189 -0
  160. package/src/lib/utils/vue.ts +25 -0
  161. package/tailwind.config.js +38 -0
  162. package/tsconfig.config.json +9 -0
  163. package/tsconfig.demo.json +19 -0
  164. package/tsconfig.json +16 -0
  165. package/tsconfig.lib.json +18 -0
  166. package/tsconfig.vitest.json +8 -0
  167. package/utils/breakpoints.d.ts +1 -1
  168. package/utils/component.d.ts +3 -7
  169. package/utils/component.js +5 -2
  170. package/utils/identifiable.js +5 -1
  171. package/vite.config.ts +28 -0
@@ -0,0 +1,159 @@
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 { vModelProps } from '../utils/vModel'
6
+ import NValInput, { validationProps } from './NValInput'
7
+
8
+ const nTextAreaBaseProps = {
9
+ ...vModelProps(String),
10
+ /**
11
+ * The name of the text area. Is displayed as a label above the text area.
12
+ */
13
+ name: String,
14
+ /**
15
+ * The placeholder of the text area.
16
+ */
17
+ placeholder: String,
18
+ /**
19
+ * The html autocomplete attribute of the text area.
20
+ */
21
+ autocomplete: {
22
+ type: String,
23
+ default: 'off',
24
+ },
25
+ /**
26
+ * If set to `true`, the text area is resizable in y-direction.
27
+ */
28
+ resizable: {
29
+ type: Boolean,
30
+ default: true,
31
+ },
32
+ /**
33
+ * The initial height of the text area in terms of
34
+ * how many text rows fit inside the text area.
35
+ * The height can be change if {@link nTextAreaProps.resizable} is `true`
36
+ */
37
+ rows: Number,
38
+ /**
39
+ * The maximum length of the input string. Entering longer strings are simply
40
+ * prevented, but no error message is shown to the user.
41
+ */
42
+ maxLength: Number,
43
+ /**
44
+ * If set to `true` the text area is displayed with a red border.
45
+ */
46
+ error: Boolean,
47
+ /**
48
+ * If set to `true` the text area is disabled and no interaction is possible.
49
+ */
50
+ disabled: Boolean,
51
+ /**
52
+ * If set to `true` the text area's label is hidden.
53
+ */
54
+ hideLabel: Boolean,
55
+ /**
56
+ * Adds the classes directly to the input (e.g. for shadow).
57
+ */
58
+ inputClass: String,
59
+ /**
60
+ * This is called when the text area reveices focus.
61
+ */
62
+ onFocus: Function as PropType<() => void>,
63
+ /**
64
+ * This is called when the text area looses focus.
65
+ */
66
+ onBlur: Function as PropType<() => void>,
67
+ ...nToolTipPropsForImplementor,
68
+ } as const
69
+
70
+ export const nTextAreaProps = {
71
+ ...nTextAreaBaseProps,
72
+ ...validationProps,
73
+ } as const
74
+
75
+ export type NTextAreaExposed = {
76
+ /**
77
+ * Request focus on the text area.
78
+ */
79
+ focus(): void
80
+ }
81
+
82
+ const Component = createComponent('NTextArea', nTextAreaProps, (props, context) => {
83
+ const textAreaRef = ref<NTextAreaExposed>()
84
+ const exposed: NTextAreaExposed = {
85
+ focus: () => textAreaRef.value?.focus(),
86
+ }
87
+ context.expose(exposed)
88
+
89
+ return () => (
90
+ <NValInput
91
+ {...props}
92
+ input={({ error, onBlur, onUpdateValue }) => (
93
+ <NTextAreaBase ref={textAreaRef} {...{ ...props, error, onBlur, onUpdateValue }} />
94
+ )}
95
+ />
96
+ )
97
+ })
98
+
99
+ export { Component as NTextArea, Component as default }
100
+
101
+ /**
102
+ * The `NTextArea` wraps the html text area with all the features from {@link NInput} and {@link NValInput}.
103
+ */
104
+ const NTextAreaBase = createComponent('NTextAreaBase', nTextAreaBaseProps, (props, context) => {
105
+ const textAreaRef = ref<HTMLTextAreaElement>()
106
+ const exposed: NTextAreaExposed = {
107
+ focus: () => textAreaRef.value?.focus(),
108
+ }
109
+ context.expose(exposed)
110
+
111
+ return () => (
112
+ <div>
113
+ {props.name && !props.hideLabel && (
114
+ <label
115
+ for={props.name}
116
+ class={['block text-sm font-medium mb-1', props.disabled ? 'text-default-300' : 'text-default-700']}
117
+ >
118
+ {props.name}
119
+ </label>
120
+ )}
121
+ <NTooltip block {...mapTooltipProps(props)}>
122
+ <div class="relative">
123
+ <textarea
124
+ ref={textAreaRef}
125
+ name={props.name}
126
+ value={props.value}
127
+ onInput={event => props.onUpdateValue?.((event.target as HTMLInputElement).value)}
128
+ placeholder={props.placeholder}
129
+ autocomplete={props.autocomplete}
130
+ disabled={props.disabled}
131
+ rows={props.rows}
132
+ maxlength={props.maxLength}
133
+ onFocus={() => props.onFocus?.()}
134
+ onBlur={() => props.onBlur?.()}
135
+ onInvalid={event => event.preventDefault()}
136
+ class={[
137
+ 'block w-full rounded-md border focus:outline-none focus:ring-1 ',
138
+ props.disabled
139
+ ? 'text-default-500 placeholder-default-300 bg-default-50'
140
+ : 'text-default-900 placeholder-default-400 ',
141
+ props.error
142
+ ? 'border-red-500 focus:border-red-500 focus:ring-red-500 pr-10'
143
+ : 'border-default-300 focus:border-primary-500 focus:ring-primary-500',
144
+ props.resizable ? 'resize-y' : 'resize-none',
145
+ props.inputClass,
146
+ ]}
147
+ />
148
+
149
+ <div
150
+ class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"
151
+ v-show={props.error}
152
+ >
153
+ <ExclamationCircleIcon class="h-5 w-5 text-red-700" aria-hidden="true" />
154
+ </div>
155
+ </div>
156
+ </NTooltip>
157
+ </div>
158
+ )
159
+ })
@@ -0,0 +1,37 @@
1
+ .arrow,
2
+ .arrow::before {
3
+ position: absolute;
4
+ width: 8px;
5
+ height: 8px;
6
+ }
7
+
8
+ .arrow {
9
+ visibility: hidden;
10
+ }
11
+
12
+ .arrow::before {
13
+ visibility: visible;
14
+ content: '';
15
+ transform: rotate(45deg);
16
+ @apply bg-white border border-default-200 border-solid;
17
+ }
18
+
19
+ .tooltip[data-popper-placement^='top'] > .arrow {
20
+ bottom: -3.8px;
21
+ @apply before:border-t-transparent before:border-l-transparent;
22
+ }
23
+
24
+ .tooltip[data-popper-placement^='bottom'] > .arrow {
25
+ top: -3.8px;
26
+ @apply before:border-b-white before:border-r-white;
27
+ }
28
+
29
+ .tooltip[data-popper-placement^='left'] > .arrow {
30
+ right: -3.8px;
31
+ @apply before:border-b-transparent before:border-l-transparent;
32
+ }
33
+
34
+ .tooltip[data-popper-placement^='right'] > .arrow {
35
+ left: -3.8px;
36
+ @apply before:border-t-transparent before:border-r-transparent;
37
+ }
@@ -0,0 +1,250 @@
1
+ import { createComponent, type ExtractedProps } from '../utils/component'
2
+ import { uniqueId } from '../utils/utils'
3
+ import type { TWMaxWidth } from '../utils/tailwind'
4
+ import { computed, onMounted, ref, watch, type PropType, onUnmounted, Transition } from 'vue'
5
+ import { createPopper, type Instance as PopperInstance } from '@popperjs/core'
6
+ import { watchRef } from '../utils/vue'
7
+ import './NTooltip.css'
8
+
9
+ export const nTooltipProps = {
10
+ /**
11
+ * The text content of the tooltip.
12
+ */
13
+ text: String,
14
+ /**
15
+ * A slot to replace the content of the tooltip. This will override the `text` prop.
16
+ */
17
+ content: Function as PropType<() => JSX.Element>,
18
+ /**
19
+ * If set to `true` the tooltip is shown constantly.
20
+ */
21
+ show: Boolean,
22
+ /**
23
+ * If set to `true` the tooltip is hidden constantly.
24
+ */
25
+ hide: Boolean,
26
+ /**
27
+ * If set to `true` the `block` class is applied to the tooltip.
28
+ * This should be set if the content in the default slot is also block.
29
+ */
30
+ block: Boolean,
31
+ /**
32
+ * The placement of the tooltip.
33
+ */
34
+ placement: {
35
+ type: String as PropType<TooltipPlacement>,
36
+ default: 'auto',
37
+ },
38
+ /**
39
+ * The maximum width of the tooltip.
40
+ */
41
+ maxWidth: {
42
+ type: String as PropType<TWMaxWidth>,
43
+ default: 'max-w-xs',
44
+ },
45
+ /**
46
+ * Adds the classes to the (invisible) wrapper element.
47
+ */
48
+ wrapperClass: String,
49
+ /**
50
+ * Adds the classes to the container of the tooltips content.
51
+ */
52
+ contentClass: String,
53
+ /**
54
+ * Adds the classes to the tooltip arrow. Make sure to use `before:` classes
55
+ * to target the arrow (which is the before element).
56
+ */
57
+ arrowClass: String,
58
+ } as const
59
+
60
+ /**
61
+ * These props are made to use on a component which implements the tooltip
62
+ * and wants it to be controllable via the own props.
63
+ * e.g. `text` is now called `tooltipText`.
64
+ * They can be mapped to the normal tooltip props with {@link mapTooltipProps}
65
+ */
66
+ export const nToolTipPropsForImplementor = {
67
+ /**
68
+ * Adds a tooltip to the component with the specified text.
69
+ * @see {@link nTooltipProps.text}
70
+ */
71
+ tooltipText: nTooltipProps.text,
72
+ /**
73
+ * A slot for the tooltip of this component.
74
+ * If the slot is set, the tooltip with the specified content is added to the component.
75
+ * @see {@link nTooltipProps.content}
76
+ */
77
+ tooltipContent: nTooltipProps.content,
78
+ /**
79
+ * @see {@link nTooltipProps.hide}
80
+ */
81
+ tooltipHide: nTooltipProps.hide,
82
+ /**
83
+ * @see {@link nTooltipProps.show}
84
+ */
85
+ tooltipShow: nTooltipProps.show,
86
+ /**
87
+ * @see {@link nTooltipProps.placement}
88
+ */
89
+ tooltipPlacement: nTooltipProps.placement,
90
+ /**
91
+ * @see {@link nTooltipProps.maxWidth}
92
+ */
93
+ tooltipMaxWidth: nTooltipProps.maxWidth,
94
+ /**
95
+ * @see {@link nTooltipProps.wrapperClass}
96
+ */
97
+ tooltipWrapperClass: nTooltipProps.wrapperClass,
98
+ /**
99
+ * @see {@link nTooltipProps.contentClass}
100
+ */
101
+ tooltipContentClass: nTooltipProps.contentClass,
102
+ /**
103
+ * @see {@link nTooltipProps.arrowClass}
104
+ */
105
+ tooltipArrowClass: nTooltipProps.arrowClass,
106
+ }
107
+
108
+ /**
109
+ * Maps the {@link nToolTipPropsForImplementor} props to normal tooltip props
110
+ * @returns an object containing the normal tooltip props.
111
+ */
112
+ export function mapTooltipProps(props: ExtractedProps<typeof nToolTipPropsForImplementor>) {
113
+ return {
114
+ text: props.tooltipText,
115
+ content: props.tooltipContent,
116
+ hide: props.tooltipHide,
117
+ show: props.tooltipShow,
118
+ placement: props.tooltipPlacement,
119
+ maxWidth: props.tooltipMaxWidth,
120
+ wrapperClass: props.tooltipWrapperClass,
121
+ contentClass: props.tooltipContentClass,
122
+ arrowClass: props.tooltipArrowClass,
123
+ }
124
+ }
125
+
126
+ /**
127
+ * The `NTooltip` is a wrapper for any component which adds a tooltip to it.
128
+ * Any component can just be passed in the default slot and a tooltip will be added to it.
129
+ * Note that this component disappears when neither the `text` nor the `content`
130
+ * prop is passed as the tooltip would then be empty.
131
+ * If this is the case, the default slot will just be rendered inside a div.
132
+ * @example
133
+ * <NTooltip text="Hello">
134
+ * <NButton />
135
+ * </NTooltip>
136
+ */
137
+ const Component = createComponent('NTooltip', nTooltipProps, (props, { slots }) => {
138
+ return () => (
139
+ <div class={[props.block ? 'block' : 'inline-block', props.wrapperClass]}>
140
+ {props.content || props.text ? (
141
+ <NTooltipBase {...props}>{slots.default?.()}</NTooltipBase>
142
+ ) : (
143
+ slots.default?.()
144
+ )}
145
+ </div>
146
+ )
147
+ })
148
+
149
+ export { Component as NTooltip, Component as default }
150
+
151
+ const NTooltipBase = createComponent('NTooltipBase', nTooltipProps, (props, { slots }) => {
152
+ let popperInstance: PopperInstance | null = null
153
+ const contentId = `content-${uniqueId()}`
154
+ const tooltipId = `tooltip-${uniqueId()}`
155
+
156
+ function createTooltip() {
157
+ const content = document.getElementById(contentId)
158
+ const tooltip = document.getElementById(tooltipId)
159
+ if (content && tooltip) {
160
+ popperInstance = createPopper(content, tooltip, {
161
+ placement: props.placement,
162
+ modifiers: [
163
+ {
164
+ name: 'offset',
165
+ options: {
166
+ offset: [0, 8],
167
+ },
168
+ },
169
+ ],
170
+ })
171
+ } else {
172
+ console.error('Could not create tooltip. HTML elements for content or tooltip were not found.')
173
+ }
174
+ }
175
+
176
+ function destroyTooltip() {
177
+ popperInstance?.destroy()
178
+ popperInstance = null
179
+ }
180
+
181
+ onMounted(createTooltip)
182
+ onUnmounted(destroyTooltip)
183
+
184
+ watch(
185
+ () => props.placement,
186
+ newPlacement => popperInstance?.setOptions({ placement: newPlacement })
187
+ )
188
+
189
+ const isHoveringContent = ref(false)
190
+ const isHoveringTooltip = ref(false)
191
+ const isHovering = computed(() => isHoveringContent.value || isHoveringTooltip.value)
192
+ const showTooltip = computed(() => props.show || (!props.hide && isHovering.value))
193
+
194
+ watchRef(showTooltip, () => popperInstance?.update())
195
+
196
+ return () => (
197
+ <>
198
+ <div
199
+ class="p-[10px] -m-[10px]"
200
+ onMouseleave={() => setTimeout(() => (isHoveringContent.value = false), 10)}
201
+ >
202
+ <div id={contentId} onMouseenter={() => (isHoveringContent.value = true)}>
203
+ {slots.default?.()}
204
+ </div>
205
+ </div>
206
+
207
+ <Transition
208
+ enterActiveClass="transition-opacity ease-out duration-100"
209
+ enterFromClass="opacity-0"
210
+ enterToClass="opacity-100"
211
+ leaveActiveClass="transition-opacity ease-in duration-75"
212
+ leaveFromClass="opacity-100"
213
+ leaveToClass="opacity-0"
214
+ >
215
+ <div
216
+ id={tooltipId}
217
+ role="tooltip"
218
+ onMouseenter={() => (isHoveringTooltip.value = true)}
219
+ onMouseleave={() => (isHoveringTooltip.value = false)}
220
+ v-show={showTooltip.value}
221
+ class={[isHovering.value ? 'z-20' : 'z-10', props.maxWidth, 'tooltip']}
222
+ >
223
+ <div
224
+ class={`bg-white rounded-md py-2 px-4 shadow-lg border-default-200 border text-sm whitespace-normal font-normal text-default-700 ${props.contentClass}`}
225
+ >
226
+ {props.content?.() || props.text}
227
+ </div>
228
+ <div data-popper-arrow class={`arrow ${props.arrowClass}`} />
229
+ </div>
230
+ </Transition>
231
+ </>
232
+ )
233
+ })
234
+
235
+ export type TooltipPlacement =
236
+ | 'auto'
237
+ | 'auto-start'
238
+ | 'auto-end'
239
+ | 'top'
240
+ | 'top-start'
241
+ | 'top-end'
242
+ | 'bottom'
243
+ | 'bottom-start'
244
+ | 'bottom-end'
245
+ | 'right'
246
+ | 'right-start'
247
+ | 'right-end'
248
+ | 'left'
249
+ | 'left-start'
250
+ | 'left-end'
@@ -0,0 +1,163 @@
1
+ import { createComponentWithSlots } from '../utils/component'
2
+ import { computed, onMounted, onUnmounted } from 'vue'
3
+ import { ref, reactive, type PropType, watch } from 'vue'
4
+ import NInput, { nInputProps, type NInputExposed } from './NInput'
5
+ import {
6
+ type ValidationRule,
7
+ type ValidationResult,
8
+ validate,
9
+ type InputValue,
10
+ required,
11
+ validResult,
12
+ } from '../utils/validation'
13
+ import type { ValidatedForm } from './ValidatedForm'
14
+
15
+ export const validationProps = {
16
+ /**
17
+ * If set to `true` this inputs validation will always succeed and all validation
18
+ * rules are ignored. Default is `false`.
19
+ */
20
+ disableValidation: Boolean,
21
+ /**
22
+ * If set to `true` this input is always valid when its value is empty.
23
+ * If set to `false` the input receives the {@link required} rule. Default is `false`.
24
+ */
25
+ optional: Boolean,
26
+ /**
27
+ * The rules which this input is checked with.
28
+ * The rules are checked sequentially and the error of the first failed rule is displayed.
29
+ * If `optional` is set to false, the rule {@link required} will be checked first.
30
+ */
31
+ rules: {
32
+ type: [Function, Array] as PropType<ValidationRule[] | ValidationRule>,
33
+ default: () => [],
34
+ },
35
+ /**
36
+ * The form, which this input will be added to.
37
+ * On initialization, this input will call {@link ValidatedForm.addInput} passing itself to the form.
38
+ */
39
+ form: Object as PropType<ValidatedForm>,
40
+ /**
41
+ * Overrides the internal error state. If set to true, it will always display an error.
42
+ */
43
+ error: Boolean,
44
+ /**
45
+ * Overrides the internal error message. If set, this message is always displayed.
46
+ */
47
+ errorMessage: String,
48
+ /**
49
+ * If set to `true` the error message is not shown.
50
+ * However, the input is still marked red if it is in an error state.
51
+ */
52
+ hideErrorMessage: Boolean,
53
+ /**
54
+ * Disables the validation on blur. Should only be used in special occasions.
55
+ */
56
+ disableBlurValidation: Boolean,
57
+ } as const
58
+
59
+ export const nValInputProps = {
60
+ ...nInputProps,
61
+ ...validationProps,
62
+ /**
63
+ * A slot to replace the input.
64
+ */
65
+ input: Function as PropType<(props: InputSlotProps) => JSX.Element>,
66
+ } as const
67
+
68
+ export type InputSlotProps = {
69
+ onBlur(): void
70
+ onUpdateValue(newValue: string): void
71
+ error: boolean
72
+ }
73
+
74
+ /**
75
+ * The exposed functions of NValInput
76
+ */
77
+ export type NValInputExposed = {
78
+ /**
79
+ * Validates the input and returns the validation result
80
+ */
81
+ validate: () => ValidationResult
82
+ /**
83
+ * Resets the validation state of the input.
84
+ */
85
+ reset: () => void
86
+ } & NInputExposed
87
+
88
+ /**
89
+ * The `NValInput` is a `NInput` with custom validation.
90
+ */
91
+ const Component = createComponentWithSlots('NValInput', nValInputProps, ['input'], (props, context) => {
92
+ const rules = computed(() => {
93
+ const otherRules = Array.isArray(props.rules) ? props.rules : [props.rules]
94
+ return props.optional ? otherRules : [required, ...otherRules]
95
+ })
96
+
97
+ const validationResult = ref<ValidationResult>()
98
+ const validateRules = (input: InputValue) => {
99
+ const result = props.disableValidation ? validResult() : validate(input, rules.value)
100
+ validationResult.value = result
101
+ return result
102
+ }
103
+
104
+ const showError = computed(() => props.error || (validationResult.value != null && !validationResult.value.isValid))
105
+ const showErrorMessage = computed(() => !props.hideErrorMessage && showError.value)
106
+ const errorMessage = computed(() => props.errorMessage || validationResult.value?.errorMessage)
107
+
108
+ const validateIfError = (value = props.value) => {
109
+ if (showError.value) validateRules(value)
110
+ }
111
+
112
+ watch(
113
+ () => props.value,
114
+ () => validateIfError()
115
+ )
116
+
117
+ watch(
118
+ () => rules.value,
119
+ () => validateIfError()
120
+ )
121
+
122
+ watch(
123
+ () => props.disableValidation,
124
+ () => validateIfError()
125
+ )
126
+
127
+ const onBlur = () => {
128
+ if (!props.disableBlurValidation) validateRules(props.value)
129
+ props.onBlur?.()
130
+ }
131
+
132
+ const onUpdateValue = (newValue: string) => {
133
+ validateIfError(newValue)
134
+ props.onUpdateValue?.(newValue)
135
+ }
136
+
137
+ const inputSlotProps: InputSlotProps = reactive({
138
+ onBlur,
139
+ onUpdateValue,
140
+ error: showError,
141
+ })
142
+
143
+ const inputRef = ref<NInputExposed>()
144
+ const expose: NValInputExposed = {
145
+ validate: () => validateRules(props.value),
146
+ reset: () => (validationResult.value = undefined),
147
+ focus: () => inputRef.value?.focus(),
148
+ }
149
+ context.expose(expose)
150
+
151
+ let assignedId: string | undefined = undefined
152
+ onMounted(() => (assignedId = props.form?.addInput(expose)))
153
+ onUnmounted(() => assignedId && props.form?.removeInput(assignedId))
154
+
155
+ return () => (
156
+ <div>
157
+ {props.input?.(inputSlotProps) || <NInput ref={inputRef} {...{ ...props, ...inputSlotProps }} />}
158
+ {showErrorMessage.value && <p class="text-red-500 text-xs mt-1">{errorMessage.value}</p>}
159
+ </div>
160
+ )
161
+ })
162
+
163
+ export { Component as NValInput, Component as default }
@@ -0,0 +1,71 @@
1
+ import { Id, type Identifiable } from '../utils/identifiable'
2
+ import { uniqueId } from '../utils/utils'
3
+ import type { ValidationResult } from '../utils/validation'
4
+ import type { NValInputExposed } from './NValInput'
5
+
6
+ /**
7
+ * A ValidatedForm groups different inputs together and provides functions, which operate on all inputs at the same time.
8
+ * With a form multiple inputs can be validated together by calling the validate function.
9
+ * @example
10
+ * const form = createValidatedForm()
11
+ * <NValInput ... form={form} />
12
+ * <NValInput ... form={form} />
13
+ * const onClick = () => form.validate()
14
+ */
15
+ export type ValidatedForm = {
16
+ /**
17
+ * Adds the input to the list of this form and returns the assigned `id`.
18
+ * If this form is passed to a `<NValInput />` via the props, will add itself to this form.
19
+ * @returns the newly assigned `id` of the added input.
20
+ * @example
21
+ * <NValInput ... form={form} />
22
+ */
23
+ addInput(input: NValInputExposed): string
24
+ /**
25
+ * Removes the input with the given `id` from this form.
26
+ */
27
+ removeInput(id: string): void
28
+ /**
29
+ * Validates all inputs of the form. If inputs are invalid they will show it visually.
30
+ * The first invalid validation result is returned.
31
+ */
32
+ validate(): ValidationResult
33
+ /**
34
+ * Resets the validation state of all inputs.
35
+ */
36
+ reset(): void
37
+ }
38
+
39
+ /**
40
+ * Creates a new ValidatedForm.
41
+ * @returns the instance of the new form.
42
+ */
43
+ export function createValidatedForm(): ValidatedForm {
44
+ return new ValidatedFormImpl()
45
+ }
46
+
47
+ class ValidatedFormImpl implements ValidatedForm {
48
+ inputs: (NValInputExposed & Identifiable)[] = []
49
+
50
+ addInput(input: NValInputExposed): string {
51
+ const id = `input-${uniqueId()}`
52
+ this.inputs.push({ id, ...input })
53
+ return id
54
+ }
55
+
56
+ removeInput(id: string): void {
57
+ Id.remove(this.inputs, id)
58
+ }
59
+
60
+ validate(): ValidationResult {
61
+ const results = this.inputs.map(input => input.validate())
62
+ // return first invalid result
63
+ for (const result of results) if (result && !result.isValid) return result
64
+ // else return valid result
65
+ return { isValid: true }
66
+ }
67
+
68
+ reset(): void {
69
+ this.inputs.forEach(input => input.reset())
70
+ }
71
+ }