@naptics/vue-collection 0.2.15 → 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.
- package/.github/workflows/build.yml +26 -0
- package/.github/workflows/deploy-demo.yml +46 -0
- package/.github/workflows/deploy-lib.yml +65 -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 +1 -70
- 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 +50 -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 +68 -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,265 @@
|
|
|
1
|
+
import { trslc } from '../i18n'
|
|
2
|
+
import { createComponent } from '../utils/component'
|
|
3
|
+
import { maxLengthSplitCenter } from '../utils/stringMaxLength'
|
|
4
|
+
import { notNullish } from '../utils/utils'
|
|
5
|
+
import { XMarkIcon } from '@heroicons/vue/24/solid'
|
|
6
|
+
import { computed, ref } from 'vue'
|
|
7
|
+
import NBadge from './NBadge'
|
|
8
|
+
import NIconButton from './NIconButton'
|
|
9
|
+
import NLink from './NLink'
|
|
10
|
+
import { vModelProps } from '../utils/vModel'
|
|
11
|
+
|
|
12
|
+
export const nDropzoneProps = {
|
|
13
|
+
...vModelProps<File[]>(Array),
|
|
14
|
+
/**
|
|
15
|
+
* A description which files are allowed for this dropzone.
|
|
16
|
+
* This should include everything the user needs to know about
|
|
17
|
+
* the file type / the extensions and the maximum size of the file.
|
|
18
|
+
* @see {@link nDropzoneProps.accept}
|
|
19
|
+
* @see {@link nDropzoneProps.maxFileSize}
|
|
20
|
+
*/
|
|
21
|
+
description: String,
|
|
22
|
+
/**
|
|
23
|
+
* The maximum amount of files which can be added to the dropzone.
|
|
24
|
+
*/
|
|
25
|
+
maxFiles: {
|
|
26
|
+
type: Number,
|
|
27
|
+
default: 1,
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Specifies which file types are accepted. The same syntax as with inputs of type file is used.
|
|
31
|
+
* Make sure to explain the requirements to the user in the {@link nDropzoneProps.description}.
|
|
32
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
|
|
33
|
+
*/
|
|
34
|
+
accept: String,
|
|
35
|
+
/**
|
|
36
|
+
* Specifies the maximum size of an individual file in bytes.
|
|
37
|
+
* Make sure to explain the max size to the user in the {@link nDropzoneProps.description}.
|
|
38
|
+
*/
|
|
39
|
+
maxFileSize: {
|
|
40
|
+
type: Number,
|
|
41
|
+
default: 100 * 1024 * 1024,
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* The maximum length of the file names.
|
|
45
|
+
* Files names longer than the specified amount of characters will be shortened.
|
|
46
|
+
* @see {@link maxLengthSplitCenter}
|
|
47
|
+
*/
|
|
48
|
+
maxLengthFileNames: {
|
|
49
|
+
type: Number,
|
|
50
|
+
default: 35,
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* A tailwind height class, which is applied to the dropzone area.
|
|
54
|
+
* It is recommended to use `min-h-*` classes,
|
|
55
|
+
* so the box is always large enough to display it's text.
|
|
56
|
+
*/
|
|
57
|
+
height: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: 'min-h-36',
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* If set to `true`, the dropzone is disabled and does not accept input anymore.
|
|
63
|
+
*/
|
|
64
|
+
disabled: Boolean,
|
|
65
|
+
} as const
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The `NDropzone` is an area where files can be added by the user by drag & drop.
|
|
69
|
+
* Files can also be selected with a file chooser by clicking on the dropzone.
|
|
70
|
+
*/
|
|
71
|
+
const Component = createComponent('NDropzone', nDropzoneProps, props => {
|
|
72
|
+
const fileError = ref<string>()
|
|
73
|
+
|
|
74
|
+
const filterAndUpdateFiles = (files: File[]) => {
|
|
75
|
+
// filter for mime type and max size
|
|
76
|
+
const fileTypeFilteredFiles = files.filter(
|
|
77
|
+
file => !props.accept || testFileWithAcceptString(props.accept, file)
|
|
78
|
+
)
|
|
79
|
+
const filteredFiles = fileTypeFilteredFiles.filter(file => file.size <= props.maxFileSize)
|
|
80
|
+
|
|
81
|
+
// filter for already existing files
|
|
82
|
+
const currentFiles = props.value || []
|
|
83
|
+
filteredFiles.forEach(file => {
|
|
84
|
+
if (currentFiles.filter(currFile => currFile.name == file.name).length == 0) currentFiles.push(file)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// slice down to max amount of files
|
|
88
|
+
const newFiles = currentFiles.slice(0, props.maxFiles)
|
|
89
|
+
|
|
90
|
+
// error handling
|
|
91
|
+
const fileTypeFilterDiff = files.length - fileTypeFilteredFiles.length
|
|
92
|
+
const fileSizeFilterDiff = fileTypeFilteredFiles.length - filteredFiles.length
|
|
93
|
+
if (newFiles.length < currentFiles.length)
|
|
94
|
+
fileError.value = trslc('vue-collection.error.too-many-files', props.maxFiles, { max: props.maxFiles })
|
|
95
|
+
else if (fileSizeFilterDiff > 0) {
|
|
96
|
+
fileError.value = trslc('vue-collection.error.file-size', fileSizeFilterDiff, { n: fileSizeFilterDiff })
|
|
97
|
+
} else if (fileTypeFilterDiff > 0)
|
|
98
|
+
fileError.value = trslc('vue-collection.error.file-type', fileTypeFilterDiff, { n: fileTypeFilterDiff })
|
|
99
|
+
else fileError.value = undefined
|
|
100
|
+
|
|
101
|
+
// update new value
|
|
102
|
+
props.onUpdateValue?.(newFiles)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const files = computed(
|
|
106
|
+
() =>
|
|
107
|
+
props.value?.map((file, index) => ({
|
|
108
|
+
index,
|
|
109
|
+
name: maxLengthSplitCenter(file.name, props.maxLengthFileNames),
|
|
110
|
+
})) || []
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const removeFile = (index: number) => {
|
|
114
|
+
const newFiles = [...(props.value || [])]
|
|
115
|
+
newFiles.splice(index, 1)
|
|
116
|
+
fileError.value = undefined
|
|
117
|
+
props.onUpdateValue?.(newFiles)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const clearFiles = () => {
|
|
121
|
+
fileError.value = undefined
|
|
122
|
+
props.onUpdateValue?.([])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isDragOver = ref(false)
|
|
126
|
+
|
|
127
|
+
const onDrop = (event: DragEvent) => {
|
|
128
|
+
event.preventDefault()
|
|
129
|
+
isDragOver.value = false
|
|
130
|
+
|
|
131
|
+
if (props.disabled) return
|
|
132
|
+
const transfer = event.dataTransfer
|
|
133
|
+
if (transfer == null) return
|
|
134
|
+
|
|
135
|
+
if (transfer.items.length > 0) {
|
|
136
|
+
const items = [...transfer.items]
|
|
137
|
+
const files = items.map(item => (item.kind === 'file' ? item.getAsFile() : null)).filter(notNullish)
|
|
138
|
+
filterAndUpdateFiles(files)
|
|
139
|
+
} else {
|
|
140
|
+
filterAndUpdateFiles([...transfer.files])
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const onDragOver = (event: DragEvent) => {
|
|
145
|
+
event.preventDefault()
|
|
146
|
+
isDragOver.value = true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const onDragLeave = () => {
|
|
150
|
+
isDragOver.value = false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const onClick = () => {
|
|
154
|
+
fileInput.value?.click()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const fileInput = ref<HTMLInputElement>()
|
|
158
|
+
|
|
159
|
+
const onInputFilesChanged = () => {
|
|
160
|
+
const input = fileInput.value
|
|
161
|
+
if (input != null) {
|
|
162
|
+
if (input.files != null) filterAndUpdateFiles([...input.files])
|
|
163
|
+
input.value = ''
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return () => (
|
|
168
|
+
<div>
|
|
169
|
+
<button
|
|
170
|
+
class={[
|
|
171
|
+
'block w-full rounded-md border-dashed border-2 focus-visible:border-primary-500 focus:outline-none ',
|
|
172
|
+
'flex flex-col items-center justify-center text-center text-sm select-none p-4',
|
|
173
|
+
!props.disabled ? 'hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700' : '',
|
|
174
|
+
props.disabled
|
|
175
|
+
? 'border-default-300 bg-default-50/50 text-default-500/30'
|
|
176
|
+
: isDragOver.value
|
|
177
|
+
? 'border-primary-300 bg-primary-50 text-primary-700'
|
|
178
|
+
: 'border-default-300 bg-default-50 text-default-500',
|
|
179
|
+
props.height,
|
|
180
|
+
]}
|
|
181
|
+
onDrop={onDrop}
|
|
182
|
+
onDragover={onDragOver}
|
|
183
|
+
onDragleave={onDragLeave}
|
|
184
|
+
onClick={onClick}
|
|
185
|
+
type="button"
|
|
186
|
+
disabled={props.disabled}
|
|
187
|
+
>
|
|
188
|
+
<input
|
|
189
|
+
type="file"
|
|
190
|
+
class="hidden"
|
|
191
|
+
ref={fileInput}
|
|
192
|
+
multiple={props.maxFiles > 1}
|
|
193
|
+
accept={props.accept}
|
|
194
|
+
onChange={onInputFilesChanged}
|
|
195
|
+
onClick={event => event.stopPropagation()}
|
|
196
|
+
/>
|
|
197
|
+
|
|
198
|
+
{/* Counterweight */}
|
|
199
|
+
<div class="flex-grow mb-2" />
|
|
200
|
+
|
|
201
|
+
<span class="font-medium">
|
|
202
|
+
{trslc('vue-collection.text.drag-n-drop-files', props.maxFiles, { n: props.maxFiles })}
|
|
203
|
+
</span>
|
|
204
|
+
<span>{props.description}</span>
|
|
205
|
+
|
|
206
|
+
<div class="flex-grow mt-2 flex items-end justify-center text-red-500 font-medium">
|
|
207
|
+
<span>{fileError.value}</span>
|
|
208
|
+
</div>
|
|
209
|
+
</button>
|
|
210
|
+
|
|
211
|
+
<div class="mt-2 space-y-1">
|
|
212
|
+
<div class="flex flex-wrap gap-2 ">
|
|
213
|
+
{files.value.map(file => (
|
|
214
|
+
<NBadge key={file.index} color="default" allCaps={false} textSize="text-xs">
|
|
215
|
+
<div class="flex items-center space-x-2">
|
|
216
|
+
<span>{file.name}</span>
|
|
217
|
+
<NIconButton
|
|
218
|
+
icon={XMarkIcon}
|
|
219
|
+
shade={900}
|
|
220
|
+
size={3}
|
|
221
|
+
onClick={() => removeFile(file.index)}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
</NBadge>
|
|
225
|
+
))}
|
|
226
|
+
|
|
227
|
+
<div class="flex-grow text-sm text-default-500 flex items-end justify-end text-right">
|
|
228
|
+
<span>
|
|
229
|
+
<span>
|
|
230
|
+
{trslc('vue-collection.text.files-selected', files.value.length, {
|
|
231
|
+
n: files.value.length,
|
|
232
|
+
})}
|
|
233
|
+
</span>
|
|
234
|
+
{files.value.length > 0 && (
|
|
235
|
+
<>
|
|
236
|
+
<span> </span>
|
|
237
|
+
<NLink color="default" onClick={clearFiles}>
|
|
238
|
+
{trslc('vue-collection.action.clear-files', files.value.length)}
|
|
239
|
+
</NLink>
|
|
240
|
+
</>
|
|
241
|
+
)}
|
|
242
|
+
</span>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
export { Component as NDropzone, Component as default }
|
|
251
|
+
|
|
252
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
|
|
253
|
+
const MIME_FORMAT = /^(image|audio|application|video|text)\/\*$/
|
|
254
|
+
const EXTENSION_FORMAT = /^\.\w{2,20}$/
|
|
255
|
+
function testFileWithAcceptString(accept: string, file: File): boolean {
|
|
256
|
+
const splitted = accept.split(',').map(pattern => pattern.trim())
|
|
257
|
+
for (const pattern of splitted) {
|
|
258
|
+
if (MIME_FORMAT.test(pattern)) {
|
|
259
|
+
if (RegExp(`^${pattern.substring(0, pattern.length - 2)}\\/.{2,}$`).test(file.type)) return true
|
|
260
|
+
} else if (EXTENSION_FORMAT.test(pattern)) {
|
|
261
|
+
if (RegExp(`^.*${pattern}$`).test(file.name)) return true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createComponent } from '../utils/component'
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import type { ValidatedForm } from './ValidatedForm'
|
|
4
|
+
|
|
5
|
+
export const nFormProps = {
|
|
6
|
+
/**
|
|
7
|
+
* The {@link ValidatedForm} which will be used to validate the inputs.
|
|
8
|
+
* All inputs in this forms hierarchy should be added to the {@link ValidatedForm}.
|
|
9
|
+
*/
|
|
10
|
+
form: Object as PropType<ValidatedForm>,
|
|
11
|
+
/**
|
|
12
|
+
* This is called, when a button of type `submit` in the hierarchy of this view is clicked
|
|
13
|
+
* and when the validation of the `form` was successful.
|
|
14
|
+
*/
|
|
15
|
+
onSubmit: Function as PropType<() => void>,
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The `NForm` should be used to wrap multiple inputs.
|
|
20
|
+
* If it contains a button of type `submit` in it's hierarchy,
|
|
21
|
+
* it catches the submit event and passes it to the {@link ValidatedForm} in its `form` prop.
|
|
22
|
+
*/
|
|
23
|
+
const Component = createComponent('NForm', nFormProps, (props, context) => {
|
|
24
|
+
const onSubmit = (event: Event) => {
|
|
25
|
+
event.preventDefault()
|
|
26
|
+
if (!props.form || props.form.validate().isValid) props.onSubmit?.()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return () => <form onSubmit={onSubmit}>{context.slots.default?.()}</form>
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export { Component as NForm, Component as default }
|
|
@@ -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,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 }
|