@naptics/vue-collection 0.2.14 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +9 -221
- 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 +9 -127
- 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 +15 -227
- package/components/NModal.js +16 -2
- 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,78 @@
|
|
|
1
|
+
import { trsl } from '../i18n'
|
|
2
|
+
import { createComponent } from '../utils/component'
|
|
3
|
+
import { MagnifyingGlassIcon } from '@heroicons/vue/24/solid'
|
|
4
|
+
import { ref, type PropType } from 'vue'
|
|
5
|
+
import { vModelProps } from '../utils/vModel'
|
|
6
|
+
|
|
7
|
+
export const nSearchbarProps = {
|
|
8
|
+
...vModelProps(String),
|
|
9
|
+
/**
|
|
10
|
+
* The placeholder of the search-bar.
|
|
11
|
+
*/
|
|
12
|
+
placeholder: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: trsl('vue-collection.action.search'),
|
|
15
|
+
},
|
|
16
|
+
/**
|
|
17
|
+
* If set to `true` the search-bar is displayed smaller.
|
|
18
|
+
*/
|
|
19
|
+
small: Boolean,
|
|
20
|
+
/**
|
|
21
|
+
* Adds the classes directly to the input (e.g. for shadow).
|
|
22
|
+
*/
|
|
23
|
+
inputClass: String,
|
|
24
|
+
/**
|
|
25
|
+
* This is called when the search-bar receives focus.
|
|
26
|
+
*/
|
|
27
|
+
onFocus: Function as PropType<() => void>,
|
|
28
|
+
/**
|
|
29
|
+
* This is called when the search-bar looses focus.
|
|
30
|
+
*/
|
|
31
|
+
onBlur: Function as PropType<() => void>,
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
export type NSearchbarExposed = {
|
|
35
|
+
/**
|
|
36
|
+
* Request focus on the search-bar.
|
|
37
|
+
*/
|
|
38
|
+
focus(): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The `NSearchbar` is a styled input with a search icon.
|
|
43
|
+
*/
|
|
44
|
+
const Component = createComponent('NSearchbar', nSearchbarProps, (props, context) => {
|
|
45
|
+
const inputRef = ref<HTMLInputElement>()
|
|
46
|
+
const exposed: NSearchbarExposed = {
|
|
47
|
+
focus: () => {
|
|
48
|
+
inputRef.value?.focus()
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
context.expose(exposed)
|
|
52
|
+
|
|
53
|
+
return () => (
|
|
54
|
+
<div class="relative">
|
|
55
|
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
56
|
+
<MagnifyingGlassIcon class="h-5 w-5 text-default-400" aria-hidden="true" />
|
|
57
|
+
</div>
|
|
58
|
+
<input
|
|
59
|
+
ref={inputRef}
|
|
60
|
+
value={props.value}
|
|
61
|
+
onInput={event => props.onUpdateValue?.((event.target as HTMLInputElement).value)}
|
|
62
|
+
type="text"
|
|
63
|
+
name="search"
|
|
64
|
+
placeholder={props.placeholder}
|
|
65
|
+
autocomplete="off"
|
|
66
|
+
class={[
|
|
67
|
+
'block w-full pl-10 pr-4 rounded-md border focus:outline-none focus:ring-1 transition placeholder-default-400 border-default-300 focus:border-primary-500 focus:ring-primary-500',
|
|
68
|
+
props.small ? 'py-1' : 'py-2',
|
|
69
|
+
props.inputClass,
|
|
70
|
+
]}
|
|
71
|
+
onFocus={props.onFocus}
|
|
72
|
+
onBlur={props.onBlur}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export { Component as NSearchbar, Component as default }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createComponentWithSlots } from '../utils/component'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { vModelProps } from '../utils/vModel'
|
|
4
|
+
|
|
5
|
+
import NSearchbar, { nSearchbarProps, type NSearchbarExposed } from './NSearchbar'
|
|
6
|
+
import NSuggestionList, { nSuggestionListPropsForConfig } from './NSuggestionList'
|
|
7
|
+
|
|
8
|
+
export const nSearchbarListProps = {
|
|
9
|
+
...nSuggestionListPropsForConfig,
|
|
10
|
+
...vModelProps(String),
|
|
11
|
+
/**
|
|
12
|
+
* @see {@link nSearchbarProps.placeholder}
|
|
13
|
+
*/
|
|
14
|
+
placeholder: nSearchbarProps.placeholder,
|
|
15
|
+
/**
|
|
16
|
+
* Adds the classes directly to the input (e.g. for shadow).
|
|
17
|
+
*/
|
|
18
|
+
inputClass: String,
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The `NSearchbarList` is a {@link NSearchbar} with a {@link NSuggestionList}.
|
|
23
|
+
*/
|
|
24
|
+
const Component = createComponentWithSlots('NSearchbarList', nSearchbarListProps, ['listItem'], props => {
|
|
25
|
+
const searchbarRef = ref<NSearchbarExposed>()
|
|
26
|
+
|
|
27
|
+
return () => (
|
|
28
|
+
<NSuggestionList
|
|
29
|
+
{...props}
|
|
30
|
+
inputValue={props.value || ''}
|
|
31
|
+
input={({ onFocus, onBlur }) => (
|
|
32
|
+
<NSearchbar
|
|
33
|
+
ref={searchbarRef}
|
|
34
|
+
value={props.value}
|
|
35
|
+
onUpdateValue={props.onUpdateValue}
|
|
36
|
+
placeholder={props.placeholder}
|
|
37
|
+
inputClass={`shadow-lg ${props.inputClass}`}
|
|
38
|
+
onFocus={onFocus}
|
|
39
|
+
onBlur={onBlur}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
onRequestInputFocus={() => searchbarRef.value?.focus()}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export { Component as NSearchbarList, Component as default }
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { trsl } from '../i18n'
|
|
2
|
+
import { createComponent } from '../utils/component'
|
|
3
|
+
import type { Identifiable } from '../utils/identifiable'
|
|
4
|
+
import { ref, type PropType } from 'vue'
|
|
5
|
+
import NTooltip, { mapTooltipProps, nToolTipPropsForImplementor } from './NTooltip'
|
|
6
|
+
import NValInput, { nValInputProps, type NValInputExposed } from './NValInput'
|
|
7
|
+
|
|
8
|
+
export const nSelectProps = {
|
|
9
|
+
/**
|
|
10
|
+
* The id of the currently selected option of this input.
|
|
11
|
+
*/
|
|
12
|
+
value: String,
|
|
13
|
+
/**
|
|
14
|
+
* This is called with the newly selected id when the selection has changed.
|
|
15
|
+
* If no id is selected, the empty string is passed, in order to
|
|
16
|
+
* match the API of all other inputs who never pass `undefined`.
|
|
17
|
+
*/
|
|
18
|
+
onUpdateValue: Function as PropType<(newValue: string) => void>,
|
|
19
|
+
/**
|
|
20
|
+
* The different options which can be selected.
|
|
21
|
+
*/
|
|
22
|
+
options: {
|
|
23
|
+
type: Array as PropType<SelectionOption[]>,
|
|
24
|
+
default: () => [],
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* The value which is shown in the empty state.
|
|
28
|
+
* The "nothing-option" will be called like this.
|
|
29
|
+
*/
|
|
30
|
+
placeholder: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: () => trsl('vue-collection.action.select'),
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* @see {@link nValInputProps.name}
|
|
36
|
+
*/
|
|
37
|
+
name: nValInputProps.name,
|
|
38
|
+
/**
|
|
39
|
+
* @see {@link nValInputProps.optional}
|
|
40
|
+
*/
|
|
41
|
+
optional: nValInputProps.optional,
|
|
42
|
+
/**
|
|
43
|
+
* @see {@link nValInputProps.disabled}
|
|
44
|
+
*/
|
|
45
|
+
disabled: nValInputProps.disabled,
|
|
46
|
+
/**
|
|
47
|
+
* @see {@link nValInputProps.form}
|
|
48
|
+
*/
|
|
49
|
+
form: nValInputProps.form,
|
|
50
|
+
/**
|
|
51
|
+
* @see {@link nValInputProps.hideLabel}
|
|
52
|
+
*/
|
|
53
|
+
hideLabel: nValInputProps.hideLabel,
|
|
54
|
+
/**
|
|
55
|
+
* @see {@link nValInputProps.inputClass}
|
|
56
|
+
*/
|
|
57
|
+
inputClass: nValInputProps.inputClass,
|
|
58
|
+
...nToolTipPropsForImplementor,
|
|
59
|
+
} as const
|
|
60
|
+
|
|
61
|
+
export type SelectionOption = Identifiable & { label: string }
|
|
62
|
+
export type NSelectExposed = NValInputExposed
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The `NSelect` is a styled html select-input.
|
|
66
|
+
*/
|
|
67
|
+
const Component = createComponent('NSelect', nSelectProps, (props, context) => {
|
|
68
|
+
const inputRef = ref<NSelectExposed>()
|
|
69
|
+
const exposed: NSelectExposed = {
|
|
70
|
+
focus: () => inputRef.value?.focus(),
|
|
71
|
+
validate: () => {
|
|
72
|
+
if (inputRef.value == null) throw new Error('Can not validate NSelect as its input was undefined')
|
|
73
|
+
return inputRef.value.validate()
|
|
74
|
+
},
|
|
75
|
+
reset: () => inputRef.value?.reset(),
|
|
76
|
+
}
|
|
77
|
+
context.expose(exposed)
|
|
78
|
+
|
|
79
|
+
return () => (
|
|
80
|
+
<NValInput
|
|
81
|
+
ref={inputRef}
|
|
82
|
+
{...props}
|
|
83
|
+
input={slotProps => (
|
|
84
|
+
<>
|
|
85
|
+
{props.name && !props.hideLabel && (
|
|
86
|
+
<label
|
|
87
|
+
for={props.name}
|
|
88
|
+
class={[
|
|
89
|
+
'block text-sm font-medium mb-1',
|
|
90
|
+
props.disabled ? 'text-default-300' : 'text-default-700',
|
|
91
|
+
]}
|
|
92
|
+
>
|
|
93
|
+
{props.name}
|
|
94
|
+
</label>
|
|
95
|
+
)}
|
|
96
|
+
<NTooltip block {...mapTooltipProps(props)}>
|
|
97
|
+
<select
|
|
98
|
+
name={props.name}
|
|
99
|
+
disabled={props.disabled}
|
|
100
|
+
value={props.value}
|
|
101
|
+
onChange={event => slotProps.onUpdateValue((event.target as HTMLInputElement).value)}
|
|
102
|
+
onBlur={slotProps.onBlur}
|
|
103
|
+
class={[
|
|
104
|
+
'block w-full py-2 pl-4 pr-10 rounded-md border focus:outline-none focus:ring-1',
|
|
105
|
+
props.disabled ? 'text-default-300 ' : 'text-default-900 ',
|
|
106
|
+
slotProps.error
|
|
107
|
+
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
|
|
108
|
+
: 'border-default-300 focus:border-primary-500 focus:ring-primary-500',
|
|
109
|
+
props.inputClass,
|
|
110
|
+
]}
|
|
111
|
+
>
|
|
112
|
+
<option disabled={!props.optional} selected={!props.value} value="">
|
|
113
|
+
{props.placeholder}
|
|
114
|
+
</option>
|
|
115
|
+
{props.options.map(option => (
|
|
116
|
+
<option key={option.id} value={option.id} selected={props.value == option.id}>
|
|
117
|
+
{option.label}
|
|
118
|
+
</option>
|
|
119
|
+
))}
|
|
120
|
+
</select>
|
|
121
|
+
</NTooltip>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
export { Component as NSelect, Component as default }
|
|
@@ -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 }
|