@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,216 @@
|
|
|
1
|
+
import { trsl } from '../i18n'
|
|
2
|
+
import type { Identifiable } from '../utils/identifiable'
|
|
3
|
+
import { createComponentWithSlots } from '../utils/component'
|
|
4
|
+
import { computed, ref, type PropType } from 'vue'
|
|
5
|
+
import NLoadingIndicator from './NLoadingIndicator'
|
|
6
|
+
import type { AnyObject } from '../utils/utils'
|
|
7
|
+
|
|
8
|
+
export const nSuggestionListPropsForConfig = {
|
|
9
|
+
/**
|
|
10
|
+
* The items which are available to show in the list. The first `maxItems` will be displayed.
|
|
11
|
+
*/
|
|
12
|
+
items: {
|
|
13
|
+
type: Array as PropType<Array<SuggestionItem>>,
|
|
14
|
+
default: () => [],
|
|
15
|
+
},
|
|
16
|
+
/**
|
|
17
|
+
* The maximum items which are displayed in the list.
|
|
18
|
+
*/
|
|
19
|
+
maxItems: {
|
|
20
|
+
type: Number,
|
|
21
|
+
default: () => 8,
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* If set to `true` the list is hidden.
|
|
25
|
+
*/
|
|
26
|
+
hideList: Boolean,
|
|
27
|
+
/**
|
|
28
|
+
* If set to `true` the list shows a loading indicator when the `items` array is empty.
|
|
29
|
+
*/
|
|
30
|
+
loading: Boolean,
|
|
31
|
+
/**
|
|
32
|
+
* This is called with the id of the selected item.
|
|
33
|
+
*/
|
|
34
|
+
onSelect: Function as PropType<(id: string) => void>,
|
|
35
|
+
/**
|
|
36
|
+
* The slot for every item of the list.
|
|
37
|
+
*/
|
|
38
|
+
listItem: Function as PropType<(props: ItemSlotProps) => JSX.Element>,
|
|
39
|
+
/**
|
|
40
|
+
* This function is called, when the input and the suggestion list are really blurred.
|
|
41
|
+
* This means, it's not just the input temporarly beeing blurred because the user clicks on the item list,
|
|
42
|
+
* but the focus has completely disappeared from the input and the list.
|
|
43
|
+
*/
|
|
44
|
+
onRealBlur: Function as PropType<() => void>,
|
|
45
|
+
} as const
|
|
46
|
+
|
|
47
|
+
export const nSuggestionListPropsForInput = {
|
|
48
|
+
/**
|
|
49
|
+
* The slot for the input, which will be enhanced with the suggestion list.
|
|
50
|
+
*/
|
|
51
|
+
input: {
|
|
52
|
+
type: Function as PropType<(props: InputSlotProps) => JSX.Element>,
|
|
53
|
+
required: true,
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* When this function is called, the parent is required to call focus() on the input element.
|
|
57
|
+
* It won't work properly if the parent does not request focus on the input.
|
|
58
|
+
*/
|
|
59
|
+
onRequestInputFocus: {
|
|
60
|
+
type: Function as PropType<() => void>,
|
|
61
|
+
required: true,
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* The current value of the input. This is just needed to display the «No results found for {value}» message.
|
|
65
|
+
*/
|
|
66
|
+
inputValue: {
|
|
67
|
+
type: String,
|
|
68
|
+
required: true,
|
|
69
|
+
},
|
|
70
|
+
} as const
|
|
71
|
+
|
|
72
|
+
export const nSuggestionListProps = {
|
|
73
|
+
...nSuggestionListPropsForConfig,
|
|
74
|
+
...nSuggestionListPropsForInput,
|
|
75
|
+
} as const
|
|
76
|
+
|
|
77
|
+
export type InputSlotProps = {
|
|
78
|
+
/**
|
|
79
|
+
* Should be called when the input receives focus.
|
|
80
|
+
*/
|
|
81
|
+
onFocus(): void
|
|
82
|
+
/**
|
|
83
|
+
* Should be called when the input is blurred.
|
|
84
|
+
*/
|
|
85
|
+
onBlur(): void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type ItemSlotProps<T extends Identifiable = SuggestionItem> = {
|
|
89
|
+
/**
|
|
90
|
+
* The current item of the list
|
|
91
|
+
*/
|
|
92
|
+
item: T
|
|
93
|
+
/**
|
|
94
|
+
* Is true, when the current item is highlighted ("hovered" with the keys)
|
|
95
|
+
*/
|
|
96
|
+
highlighted: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type SuggestionItem = Identifiable & { label?: string } & AnyObject
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The `NSuggestionList` can be added to an input and adds a list below it which is shown when the input is focused.
|
|
103
|
+
*/
|
|
104
|
+
const Component = createComponentWithSlots('NSuggestionList', nSuggestionListProps, ['input', 'listItem'], props => {
|
|
105
|
+
const selectedIndex = ref<number | null>(null)
|
|
106
|
+
const displayItems = computed(() => props.items.slice(0, props.maxItems))
|
|
107
|
+
|
|
108
|
+
const isInFocus = ref(false)
|
|
109
|
+
const showList = computed(() => isInFocus.value && !props.hideList)
|
|
110
|
+
|
|
111
|
+
let listButtonClicked = false
|
|
112
|
+
|
|
113
|
+
const onFocus = () => (isInFocus.value = true)
|
|
114
|
+
const onListMouseDown = () => (listButtonClicked = true)
|
|
115
|
+
const onListMouseLeave = () => props.onRequestInputFocus()
|
|
116
|
+
|
|
117
|
+
const onBlur = () => {
|
|
118
|
+
if (!listButtonClicked) {
|
|
119
|
+
isInFocus.value = false
|
|
120
|
+
props.onRealBlur?.()
|
|
121
|
+
}
|
|
122
|
+
listButtonClicked = false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const onSelect = (id: string) => {
|
|
126
|
+
props.onSelect?.(id)
|
|
127
|
+
props.onRequestInputFocus()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const keydown = (event: KeyboardEvent) => {
|
|
131
|
+
if (event.key == 'ArrowDown') {
|
|
132
|
+
event.preventDefault()
|
|
133
|
+
nextItem()
|
|
134
|
+
} else if (event.key == 'ArrowUp') {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
previoiusItem()
|
|
137
|
+
} else if (event.key == 'Enter') {
|
|
138
|
+
event.preventDefault()
|
|
139
|
+
const index = selectedIndex.value
|
|
140
|
+
if (index != null && index < displayItems.value.length) {
|
|
141
|
+
const item = displayItems.value[index]
|
|
142
|
+
if (item) {
|
|
143
|
+
onSelect(item.id)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const nextItem = () => {
|
|
150
|
+
adjustIndexToSize()
|
|
151
|
+
const currentIndex = selectedIndex.value
|
|
152
|
+
let nextIndex: number | null = currentIndex == null ? 0 : currentIndex + 1
|
|
153
|
+
if (nextIndex >= displayItems.value.length) nextIndex = null
|
|
154
|
+
selectedIndex.value = nextIndex
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const previoiusItem = () => {
|
|
158
|
+
adjustIndexToSize()
|
|
159
|
+
const currentIndex = selectedIndex.value
|
|
160
|
+
let previousIndex: number | null = currentIndex == null ? displayItems.value.length - 1 : currentIndex - 1
|
|
161
|
+
if (previousIndex < 0) previousIndex = null
|
|
162
|
+
selectedIndex.value = previousIndex
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const adjustIndexToSize = () => {
|
|
166
|
+
if (selectedIndex.value != null && selectedIndex.value >= displayItems.value.length) {
|
|
167
|
+
selectedIndex.value = null
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return () => (
|
|
172
|
+
<div onKeydown={keydown}>
|
|
173
|
+
{props.input({ onFocus, onBlur })}
|
|
174
|
+
<div class="relative">
|
|
175
|
+
{showList.value && (
|
|
176
|
+
<div class="bg-white rounded-md shadow-lg p-2 absolute top-2 left-0 min-w-full z-10">
|
|
177
|
+
<ul>
|
|
178
|
+
{displayItems.value.map((item, index) => (
|
|
179
|
+
<li
|
|
180
|
+
key={item.id}
|
|
181
|
+
class={[
|
|
182
|
+
'focus:outline-none hover:bg-default-50 rounded-md select-none p-2 cursor-pointer',
|
|
183
|
+
selectedIndex.value === index ? 'bg-default-50' : '',
|
|
184
|
+
]}
|
|
185
|
+
onMousedown={onListMouseDown}
|
|
186
|
+
onMouseleave={onListMouseLeave}
|
|
187
|
+
onClick={() => onSelect(item.id)}
|
|
188
|
+
>
|
|
189
|
+
{props.listItem?.({ item, highlighted: selectedIndex.value === index }) ||
|
|
190
|
+
item.label}
|
|
191
|
+
</li>
|
|
192
|
+
))}
|
|
193
|
+
|
|
194
|
+
{displayItems.value.length == 0 && (
|
|
195
|
+
<div class="p-2 text-sm font-medium text-default-700">
|
|
196
|
+
{props.loading ? (
|
|
197
|
+
<div class="flex items-center space-x-2">
|
|
198
|
+
<NLoadingIndicator size={6} />
|
|
199
|
+
<span> {trsl('vue-collection.text.loading-search-results')}</span>
|
|
200
|
+
</div>
|
|
201
|
+
) : (
|
|
202
|
+
<div>
|
|
203
|
+
{trsl('vue-collection.text.no-search-results', { input: props.inputValue })}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</ul>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
export { Component as NSuggestionList, Component as default }
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { isWidthBreakpoint, type TWBreakpoint } from '../utils/breakpoints'
|
|
2
|
+
import { createComponent } from '../utils/component'
|
|
3
|
+
import { ChevronLeftIcon } from '@heroicons/vue/24/solid'
|
|
4
|
+
import { computed, Fragment, ref, type PropType, watch } from 'vue'
|
|
5
|
+
import NIconButton from './NIconButton'
|
|
6
|
+
import './NTable.css'
|
|
7
|
+
|
|
8
|
+
export type TableItem = string | number | (() => JSX.Element)
|
|
9
|
+
|
|
10
|
+
export type TableHeading = {
|
|
11
|
+
/**
|
|
12
|
+
* The key of the table heading. This should match the key of the items.
|
|
13
|
+
*/
|
|
14
|
+
key: string
|
|
15
|
+
/**
|
|
16
|
+
* The label of the table heading. If not set, no label is displayed.
|
|
17
|
+
*/
|
|
18
|
+
label?: TableItem
|
|
19
|
+
/**
|
|
20
|
+
* If set to `true` the whole column of this heading will be emphasized.
|
|
21
|
+
*/
|
|
22
|
+
emph?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* This classes will be directly set to all cells in the column of this heading.
|
|
25
|
+
*/
|
|
26
|
+
cellClass?: string
|
|
27
|
+
/**
|
|
28
|
+
* This classes will be directly set on all cells of this heading, when they show up in the details.
|
|
29
|
+
*/
|
|
30
|
+
detailsClass?: string
|
|
31
|
+
/**
|
|
32
|
+
* If set the heading will become a details entry when the screen is smaller than the specified breakpoint.
|
|
33
|
+
*/
|
|
34
|
+
breakpoint?: TWBreakpoint
|
|
35
|
+
/**
|
|
36
|
+
* If set to `true`, this heading will always show up inside the details. If this is set the `breakpoint`
|
|
37
|
+
* property has no effect, because the items always show up as details ignoring the breakpoint.
|
|
38
|
+
*/
|
|
39
|
+
isDetail?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type TableRow = Record<string, TableItem> & {
|
|
43
|
+
/**
|
|
44
|
+
* The TableItem is used as the last element of the table and
|
|
45
|
+
* merged together in one cell with the button to toggle the details.
|
|
46
|
+
*/
|
|
47
|
+
action?: TableItem
|
|
48
|
+
/**
|
|
49
|
+
* This classes will be applied to the whole table row.
|
|
50
|
+
*/
|
|
51
|
+
rowClass?: string
|
|
52
|
+
/**
|
|
53
|
+
* This classes will be applied to the whole details section of the row.
|
|
54
|
+
*/
|
|
55
|
+
detailsClass?: string
|
|
56
|
+
}
|
|
57
|
+
const N_TABLE_ACTION_KEY = 'action'
|
|
58
|
+
|
|
59
|
+
export const nTableProps = {
|
|
60
|
+
/**
|
|
61
|
+
* The headings of the table. These define which columns are shown in the table and in which order.
|
|
62
|
+
*/
|
|
63
|
+
headings: {
|
|
64
|
+
type: Array as PropType<TableHeading[]>,
|
|
65
|
+
required: true,
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* Adds the classes to all headings at the top of the table.
|
|
69
|
+
*/
|
|
70
|
+
headingsClass: String,
|
|
71
|
+
/**
|
|
72
|
+
* Adds the classes to all headings in the details of the table.
|
|
73
|
+
*/
|
|
74
|
+
headingDetailsClass: String,
|
|
75
|
+
/**
|
|
76
|
+
* The items of the table. They consist of an array of table rows.
|
|
77
|
+
* Every tablerow is an object containing elements for the heading keys.
|
|
78
|
+
* The elements can either be a primitive value or a function which returns a {@link JSX.Element}.
|
|
79
|
+
* If the item should be treated as an action (e.g. icon-button to display at the end of the row)
|
|
80
|
+
* the dedicated key 'action' can be used.
|
|
81
|
+
* @see TableRow
|
|
82
|
+
* @example
|
|
83
|
+
* // These headings are defined
|
|
84
|
+
* const headings: TableHeading[] = [
|
|
85
|
+
* { key: 'id', label: 'ID' },
|
|
86
|
+
* { key: 'name', label: 'Name' },
|
|
87
|
+
* { key: 'status', label: 'Status' }
|
|
88
|
+
* ]
|
|
89
|
+
*
|
|
90
|
+
* // Appropriate rows for these headings
|
|
91
|
+
* const items: TableRow[] = [
|
|
92
|
+
* { id: 1, name: 'Hubert', status: () => <NBadge ... />, action: ... }, // Row 1
|
|
93
|
+
* { id: 2, name: 'Franzi', status: () => <NBadge ... />, action: ... } // Row 2
|
|
94
|
+
* ]
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
items: {
|
|
98
|
+
type: Array as PropType<TableRow[]>,
|
|
99
|
+
default: () => [],
|
|
100
|
+
},
|
|
101
|
+
} as const
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The `NTable` is a styled html table which accepts data and displays it appropriately.
|
|
105
|
+
*/
|
|
106
|
+
const Component = createComponent('NTable', nTableProps, props => {
|
|
107
|
+
const headings = computed(() => {
|
|
108
|
+
// filter out details headings
|
|
109
|
+
const headings = props.headings.filter(heading => !isHeadingDetail(heading))
|
|
110
|
+
|
|
111
|
+
// The column for actions is shown if there are details
|
|
112
|
+
// or if any of the items contain an element with the action-key.
|
|
113
|
+
if (showDetails.value || props.items.filter(row => row.action != null).length != 0) {
|
|
114
|
+
headings.push({ key: N_TABLE_ACTION_KEY })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return headings
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const details = computed(() => props.headings.filter(isHeadingDetail))
|
|
121
|
+
|
|
122
|
+
const showDetails = computed(() => details.value.length > 0)
|
|
123
|
+
|
|
124
|
+
const detailsOpen = ref<boolean[]>([])
|
|
125
|
+
const isDetailsOpen = (index: number) => detailsOpen.value[index] || false
|
|
126
|
+
const toggleDetailsOpen = (index: number) => (detailsOpen.value[index] = !detailsOpen.value[index])
|
|
127
|
+
|
|
128
|
+
// if the items change, reset all open details to closed
|
|
129
|
+
// and create correct amount of booleans for all items
|
|
130
|
+
watch(
|
|
131
|
+
() => props.items,
|
|
132
|
+
newItems => (detailsOpen.value = Array({ length: newItems.length }).map(() => false)),
|
|
133
|
+
{ immediate: true }
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return () => (
|
|
137
|
+
<div class="overflow-x-auto">
|
|
138
|
+
<table class="min-w-full text-default-500 text-sm">
|
|
139
|
+
<thead class="bg-default-50 ">
|
|
140
|
+
<tr>
|
|
141
|
+
{headings.value.map(heading => (
|
|
142
|
+
<th key={heading.key} scope="col" class={`p-4 table-heading ${props.headingsClass}`}>
|
|
143
|
+
{buildItem(heading.label)}
|
|
144
|
+
</th>
|
|
145
|
+
))}
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
|
|
149
|
+
{props.items.length > 0 &&
|
|
150
|
+
props.items.map((item, itemIndex) => (
|
|
151
|
+
<Fragment key={itemIndex}>
|
|
152
|
+
{/* First tbody is the actual table-row with the entries */}
|
|
153
|
+
<tbody
|
|
154
|
+
class={[
|
|
155
|
+
'border-default-200 border-t',
|
|
156
|
+
itemIndex % 2 === 0 ? 'bg-white' : 'bg-default-50',
|
|
157
|
+
item.rowClass,
|
|
158
|
+
]}
|
|
159
|
+
>
|
|
160
|
+
<tr>
|
|
161
|
+
{headings.value.map(heading => (
|
|
162
|
+
<td key={itemIndex + '-' + heading.key} class="p-4">
|
|
163
|
+
<div
|
|
164
|
+
class={[
|
|
165
|
+
'flex',
|
|
166
|
+
heading.emph ? 'font-medium text-default-900' : '',
|
|
167
|
+
heading.cellClass,
|
|
168
|
+
heading.key == N_TABLE_ACTION_KEY
|
|
169
|
+
? 'justify-end items-center space-x-3'
|
|
170
|
+
: '',
|
|
171
|
+
]}
|
|
172
|
+
>
|
|
173
|
+
{item[heading.key] && buildItem(item[heading.key])}
|
|
174
|
+
|
|
175
|
+
{/* Add the chevron icon-button if details are present */}
|
|
176
|
+
{heading.key == N_TABLE_ACTION_KEY && showDetails.value && (
|
|
177
|
+
<div
|
|
178
|
+
class="inline-flex transition-transform duration-200"
|
|
179
|
+
style={{
|
|
180
|
+
transform: isDetailsOpen(itemIndex)
|
|
181
|
+
? 'rotate(-90deg)'
|
|
182
|
+
: 'rotate(0deg)',
|
|
183
|
+
transformOrigin: 'center center',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<NIconButton
|
|
187
|
+
icon={ChevronLeftIcon}
|
|
188
|
+
onClick={() => toggleDetailsOpen(itemIndex)}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
</td>
|
|
194
|
+
))}
|
|
195
|
+
</tr>
|
|
196
|
+
</tbody>
|
|
197
|
+
|
|
198
|
+
{/* Second tbody are the details (only shown if present and opened) */}
|
|
199
|
+
{showDetails.value && isDetailsOpen(itemIndex) && (
|
|
200
|
+
<tbody class={[itemIndex % 2 === 0 ? 'bg-white' : 'bg-default-50', item.detailsClass]}>
|
|
201
|
+
{details.value.map((detail, detailIndex) => (
|
|
202
|
+
<tr key={`detail-${detailIndex}`}>
|
|
203
|
+
<td
|
|
204
|
+
class={[
|
|
205
|
+
'table-heading px-4 py-1',
|
|
206
|
+
props.headingDetailsClass,
|
|
207
|
+
details.value.length - 1 == detailIndex ? 'pb-4' : '',
|
|
208
|
+
]}
|
|
209
|
+
>
|
|
210
|
+
{buildItem(detail.label)}
|
|
211
|
+
</td>
|
|
212
|
+
<td
|
|
213
|
+
class={[
|
|
214
|
+
'px-4 py-1',
|
|
215
|
+
details.value[detailIndex]?.detailsClass,
|
|
216
|
+
details.value.length - 1 == detailIndex ? 'pb-4' : '',
|
|
217
|
+
]}
|
|
218
|
+
colspan={headings.value.length - 1}
|
|
219
|
+
>
|
|
220
|
+
{item[detail.key] && buildItem(item[detail.key])}
|
|
221
|
+
</td>
|
|
222
|
+
</tr>
|
|
223
|
+
))}
|
|
224
|
+
</tbody>
|
|
225
|
+
)}
|
|
226
|
+
</Fragment>
|
|
227
|
+
))}
|
|
228
|
+
</table>
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
export { Component as NTable, Component as default }
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Builds a JSX-Element out of the item
|
|
237
|
+
*/
|
|
238
|
+
function buildItem(item: TableItem | undefined) {
|
|
239
|
+
if (item === undefined) return undefined
|
|
240
|
+
else if (typeof item == 'string' || typeof item == 'number') return <>{item}</>
|
|
241
|
+
else return item()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isHeadingDetail(heading: TableHeading): boolean {
|
|
245
|
+
// take all headings which are details or below the breakpoint
|
|
246
|
+
return heading.isDetail || (heading.breakpoint !== undefined && !isWidthBreakpoint(heading.breakpoint).value)
|
|
247
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createComponent } from '../utils/component'
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import { RouterLink, type RouteLocationRaw } from 'vue-router'
|
|
4
|
+
import { nButtonProps } from './NButton'
|
|
5
|
+
|
|
6
|
+
export const nTableActionProps = {
|
|
7
|
+
/**
|
|
8
|
+
* The route of the action. If set the component will be a {@link RouterLink}.
|
|
9
|
+
*/
|
|
10
|
+
route: [String, Object] as PropType<RouteLocationRaw>,
|
|
11
|
+
/**
|
|
12
|
+
* The text of the action.
|
|
13
|
+
*/
|
|
14
|
+
text: String,
|
|
15
|
+
/**
|
|
16
|
+
* The html attribute, which indicates the type of the button.
|
|
17
|
+
*/
|
|
18
|
+
type: nButtonProps.type,
|
|
19
|
+
/**
|
|
20
|
+
* This is called when the action is clicked.
|
|
21
|
+
* It is only called when the `route` prop is not set on the action.
|
|
22
|
+
*/
|
|
23
|
+
onClick: Function as PropType<() => void>,
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The `NTableAction` is a button or {@link RouterLink} which is styled to fit into a table.
|
|
28
|
+
* It is basically styled as an emphasized text in the table.
|
|
29
|
+
*/
|
|
30
|
+
const Component = createComponent('NTableAction', nTableActionProps, (props, { slots }) => {
|
|
31
|
+
const content = () => slots.default?.() || <>{props.text}</>
|
|
32
|
+
|
|
33
|
+
const classes = [
|
|
34
|
+
'text-left font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-default-900 rounded-sm ring-offset-2 text-default-900 hover:underline hover:text-default-700',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
return () =>
|
|
38
|
+
props.route ? (
|
|
39
|
+
<RouterLink to={props.route} class={classes}>
|
|
40
|
+
{content()}
|
|
41
|
+
</RouterLink>
|
|
42
|
+
) : (
|
|
43
|
+
<button type={props.type} class={classes} onClick={props.onClick}>
|
|
44
|
+
{content()}
|
|
45
|
+
</button>
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export { Component as NTableAction, Component as default }
|
|
@@ -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
|
+
})
|