@naptics/vue-collection 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/i18n/index.js +4 -4
  2. package/index.d.ts +0 -64
  3. package/index.js +1 -66
  4. package/package.json +1 -1
  5. package/utils/breakpoints.js +21 -21
  6. package/utils/component.js +9 -17
  7. package/utils/deferred.js +12 -12
  8. package/utils/identifiable.js +29 -27
  9. package/utils/stringMaxLength.js +13 -8
  10. package/utils/tailwind.js +1 -1
  11. package/utils/utils.js +5 -5
  12. package/utils/vModel.js +73 -82
  13. package/utils/validation.js +81 -55
  14. package/utils/vue.js +5 -7
  15. package/components/NAlert.jsx +0 -69
  16. package/components/NBadge.jsx +0 -58
  17. package/components/NBreadcrub.jsx +0 -64
  18. package/components/NButton.jsx +0 -58
  19. package/components/NCheckbox.jsx +0 -38
  20. package/components/NCheckboxLabel.jsx +0 -42
  21. package/components/NCrudModal.jsx +0 -89
  22. package/components/NDialog.jsx +0 -144
  23. package/components/NDropdown.jsx +0 -92
  24. package/components/NDropzone.jsx +0 -211
  25. package/components/NForm.jsx +0 -26
  26. package/components/NFormModal.jsx +0 -48
  27. package/components/NIconButton.jsx +0 -71
  28. package/components/NIconCircle.jsx +0 -67
  29. package/components/NInput.jsx +0 -97
  30. package/components/NInputPhone.jsx +0 -32
  31. package/components/NInputSelect.jsx +0 -89
  32. package/components/NInputSuggestion.jsx +0 -48
  33. package/components/NLink.jsx +0 -58
  34. package/components/NList.jsx +0 -24
  35. package/components/NLoadingIndicator.jsx +0 -42
  36. package/components/NModal.jsx +0 -170
  37. package/components/NPagination.jsx +0 -104
  38. package/components/NSearchbar.jsx +0 -58
  39. package/components/NSearchbarList.jsx +0 -20
  40. package/components/NSelect.jsx +0 -81
  41. package/components/NSuggestionList.jsx +0 -157
  42. package/components/NTable.jsx +0 -146
  43. package/components/NTableAction.jsx +0 -35
  44. package/components/NTextArea.jsx +0 -108
  45. package/components/NTooltip.jsx +0 -161
  46. package/components/NValInput.jsx +0 -101
@@ -1,58 +0,0 @@
1
- import { createComponent, createProps } from '../utils/component';
2
- import { computed } from 'vue';
3
- import NLoadingIndicator from './NLoadingIndicator';
4
- import NTooltip, { mapTooltipProps, nToolTipPropsForImplementor } from './NTooltip';
5
- export const nButtonProps = createProps({
6
- /**
7
- * The color of the button.
8
- */
9
- color: {
10
- type: String,
11
- default: 'primary',
12
- },
13
- /**
14
- * The html attribute, which indicates the type of the button.
15
- */
16
- type: {
17
- type: String,
18
- default: 'button',
19
- },
20
- /**
21
- * If set to `true` the button is disabled and no interaction is possible.
22
- */
23
- disabled: Boolean,
24
- /**
25
- * If set to `true` the button will show a loading animation.
26
- * Setting `loading` to `true` will also disable the button.
27
- */
28
- loading: Boolean,
29
- /**
30
- * If set to `true` the button will appear smaller.
31
- */
32
- small: Boolean,
33
- /**
34
- * This is called, when the button is clicked.
35
- */
36
- onClick: Function,
37
- ...nToolTipPropsForImplementor,
38
- });
39
- /**
40
- * The `NButton` is a styled button.
41
- */
42
- export default createComponent('NButton', nButtonProps, (props, { slots }) => {
43
- const isDisabled = computed(() => props.loading || props.disabled);
44
- return () => (<NTooltip {...mapTooltipProps(props)}>
45
- <button disabled={isDisabled.value} type={props.type} class={[
46
- `block w-full font-medium rounded-md focus:outline-none focus-visible:ring-2 shadow text-${props.color}-900 relative`,
47
- isDisabled.value
48
- ? `bg-${props.color}-100 text-opacity-20 cursor-default`
49
- : `bg-${props.color}-200 hover:bg-${props.color}-300 focus-visible:ring-${props.color}-500`,
50
- props.small ? 'py-1 px-2 text-xs' : 'py-2 px-4 text-sm',
51
- ]} onClick={props.onClick}>
52
- <span class={{ 'opacity-10': props.loading }}>{slots.default?.()}</span>
53
- {props.loading && (<div class="absolute inset-0 flex items-center justify-center opacity-50">
54
- <NLoadingIndicator color={props.color} size={props.small ? 4 : 6} shade={600}/>
55
- </div>)}
56
- </button>
57
- </NTooltip>);
58
- });
@@ -1,38 +0,0 @@
1
- import { createComponent, createProps } from '../utils/component';
2
- import { vModelProps } from '../utils/vModel';
3
- import { nextTick, ref } from 'vue';
4
- export const nCheckboxProps = createProps({
5
- ...vModelProps(Boolean),
6
- /**
7
- * The color of the checkbox.
8
- */
9
- color: {
10
- type: String,
11
- default: 'primary',
12
- },
13
- /**
14
- * If set to `true` the checkbox is disabled and no interaction is possible.
15
- */
16
- disabled: Boolean,
17
- });
18
- /**
19
- * The `NCheckbox` is a styled checkbox.
20
- */
21
- export default createComponent('NCheckbox', nCheckboxProps, props => {
22
- const toggle = () => {
23
- props.onUpdateValue?.(!props.value);
24
- forceUpdate();
25
- };
26
- const checkBoxRef = ref();
27
- const updateKey = ref(0);
28
- const forceUpdate = () => {
29
- updateKey.value += 1;
30
- nextTick(() => checkBoxRef.value?.focus());
31
- };
32
- return () => (<input type="checkbox" ref={checkBoxRef} checked={props.value} disabled={props.disabled} onClick={toggle} key={updateKey.value} class={[
33
- `h-5 w-5 border-default-300 rounded focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-${props.color}-500`,
34
- props.disabled
35
- ? `cursor-default bg-default-100 text-${props.color}-200`
36
- : `cursor-pointer text-${props.color}-400`,
37
- ]}/>);
38
- });
@@ -1,42 +0,0 @@
1
- import { createComponent, createProps } from '../utils/component';
2
- import NCheckbox, { nCheckboxProps } from './NCheckbox';
3
- export const nCheckboxLabelProps = createProps({
4
- ...nCheckboxProps,
5
- /**
6
- * The title of the checkbox.
7
- */
8
- title: String,
9
- /**
10
- * The description of the checkbox.
11
- */
12
- description: String,
13
- /**
14
- * If set to `true`, a smaller margin is applied between the label and the checkbox.
15
- */
16
- compact: Boolean,
17
- });
18
- /**
19
- * The `NCheckboxLabel` is a checkbox with a title and a description.
20
- */
21
- export default createComponent('NCheckboxLabel', nCheckboxLabelProps, props => {
22
- const toggleValue = () => {
23
- if (!props.disabled)
24
- props.onUpdateValue?.(!props.value);
25
- };
26
- return () => (<div class="flex items-center">
27
- <NCheckbox {...props}/>
28
- <div class={`${props.compact ? 'ml-2' : 'ml-3'} text-sm`}>
29
- <label onClick={toggleValue} class={[
30
- 'font-medium select-none',
31
- props.disabled ? 'text-default-300' : 'text-default-700 cursor-pointer',
32
- ]}>
33
- {props.title}
34
- </label>
35
- <p class={props.disabled ? 'text-default-300' : 'text-default-500'}>
36
- <span onClick={toggleValue} class={['select-none', props.disabled ? '' : 'cursor-pointer']}>
37
- {props.description}
38
- </span>
39
- </p>
40
- </div>
41
- </div>);
42
- });
@@ -1,89 +0,0 @@
1
- import { trsl } from '../i18n';
2
- import { createComponent, createProps } from '../utils/component';
3
- import { ref } from 'vue';
4
- import NButton from './NButton';
5
- import NDialog from './NDialog';
6
- import NFormModal, { nFormModalProps } from './NFormModal';
7
- export const nCrudModalProps = createProps({
8
- ...nFormModalProps,
9
- /**
10
- * The text of the remove-button.
11
- */
12
- removeText: {
13
- type: String,
14
- default: trsl('vue-collection.action.remove'),
15
- },
16
- /**
17
- * The color of the remove-button.
18
- */
19
- removeColor: {
20
- type: String,
21
- default: 'red',
22
- },
23
- /**
24
- * The title of the dialog which appears when clicking on the remove-button.
25
- */
26
- removeDialogTitle: String,
27
- /**
28
- * The text of the dialog which appears when clicking on the remove-button.
29
- */
30
- removeDialogText: String,
31
- /**
32
- * The variant of the dialog which appears when clicking on the remove-button. Default is `remove`.
33
- */
34
- removeDialogVariant: {
35
- type: String,
36
- default: 'remove',
37
- },
38
- /**
39
- * The text of the dialog's ok-button. Is already set by the `removeDialogVariant` but can be overridden.
40
- */
41
- removeDialogOkText: String,
42
- /**
43
- * If set to `true` the modal will close itself when `onRemove` is called.
44
- */
45
- closeOnRemove: {
46
- type: Boolean,
47
- default: true,
48
- },
49
- /**
50
- * This is called, when the remove-button has been clicked and the dialog has been accepted.
51
- */
52
- onRemove: Function,
53
- });
54
- /**
55
- * The `NCrudModal` is a {@link NFormModal} which has some convenience features for a CRUD-scenario.
56
- * It has an integrated remove-button with a user-dialog to remove the editing element.
57
- * When the dialog is accepted `onRemove` is called.
58
- */
59
- export default createComponent('NCrudModal', nCrudModalProps, (props, { slots }) => {
60
- const removeDialog = ref();
61
- const remove = () => {
62
- removeDialog.value?.show().then(result => {
63
- if (result) {
64
- props.onRemove?.();
65
- if (props.closeOnRemove)
66
- props.onUpdateValue?.(false);
67
- }
68
- });
69
- };
70
- return () => (<NFormModal {...props} footer={props.footer ||
71
- (({ ok, cancel }) => (<div class="flex justify-between">
72
- <div>
73
- <NButton color={props.removeColor} onClick={remove}>
74
- {props.removeText}
75
- </NButton>
76
- </div>
77
- <div>
78
- <NButton color={props.cancelColor} onClick={cancel}>
79
- {props.cancelText}
80
- </NButton>
81
- <NButton color={props.okColor} onClick={ok} class="ml-2" disabled={props.okDisabled}>
82
- {props.okText}
83
- </NButton>
84
- </div>
85
- </div>))}>
86
- {slots.default?.()}
87
- <NDialog ref={removeDialog} variant={props.removeDialogVariant} title={props.removeDialogTitle} text={props.removeDialogText} okText={props.removeDialogOkText}/>
88
- </NFormModal>);
89
- });
@@ -1,144 +0,0 @@
1
- import { deferred } from '../utils/deferred';
2
- import { createComponent, createProps, extractProps } from '../utils/component';
3
- import { CheckIcon, ExclamationTriangleIcon, LightBulbIcon, TrashIcon } from '@heroicons/vue/24/outline';
4
- import { computed, ref } from 'vue';
5
- import NIconCircle from './NIconCircle';
6
- import NModal from './NModal';
7
- import { DialogTitle } from '@headlessui/vue';
8
- import { trsl } from '../i18n';
9
- import { vModelForRef } from '../utils/vModel';
10
- export const nDialogProps = createProps({
11
- /**
12
- * The title of the dialog.
13
- */
14
- title: String,
15
- /**
16
- * The text of the dialog.
17
- */
18
- text: String,
19
- /**
20
- * The variant of the dialog.
21
- * This determines the default icon and its color
22
- * as well as the default text and color of the ok-button.
23
- */
24
- variant: {
25
- type: String,
26
- default: 'warning',
27
- },
28
- /**
29
- * The icon of the alert. This overrides the `icon` of the `variant`.
30
- */
31
- icon: Function,
32
- /**
33
- * The color of the alert's icon. This overrides the `iconColor` of the `variant`.
34
- */
35
- iconColor: String,
36
- /**
37
- * The text of the ok-button. This overrides the `okText` of the `variant`.
38
- */
39
- okText: String,
40
- /**
41
- * The color of the ok-button. This overrides the `okColor` of the `variant`.
42
- */
43
- okColor: String,
44
- /**
45
- * The text of the cancel-button.
46
- */
47
- cancelText: {
48
- type: String,
49
- default: trsl('vue-collection.action.cancel'),
50
- },
51
- /**
52
- * The color of the cancel-button.
53
- */
54
- cancelColor: {
55
- type: String,
56
- default: 'default',
57
- },
58
- /**
59
- * If set to `true` the cancel-button is hidden.
60
- */
61
- hideCancel: Boolean,
62
- });
63
- /**
64
- * A `NDialog` is an element to interact directly with the user.
65
- * It can be controlled via a ref to prompt the user and to receive their answer.
66
- * @example
67
- * const dialogRef = ref<NDialogExposed>()
68
- * ...
69
- * const onShowDialog = () => {
70
- * dialofRef.value?.show().then(result => {
71
- * if (result) // dialog accepted
72
- * else // dialog cancelled
73
- * })
74
- * }
75
- * ...
76
- * <NDialog ref={dialogRef} />
77
- */
78
- export default createComponent('NDialog', nDialogProps, (props, context) => {
79
- const showDialog = ref(false);
80
- let deferredPromise = null;
81
- const show = () => {
82
- if (deferredPromise != null) {
83
- deferredPromise.reject('show() was called on the open dialog.');
84
- deferredPromise = null;
85
- }
86
- showDialog.value = true;
87
- deferredPromise = deferred();
88
- return deferredPromise.promise;
89
- };
90
- context.expose({ show });
91
- const resolveWith = (result) => {
92
- deferredPromise?.resolve(result);
93
- deferredPromise = null;
94
- };
95
- const ok = () => resolveWith(true);
96
- const cancel = () => resolveWith(false);
97
- const defaults = computed(() => VARIANT_DEFAULTS[props.variant]);
98
- return () => (<NModal {...vModelForRef(showDialog)} {...extractProps(props, 'cancelColor', 'cancelText', 'hideCancel')} onOk={ok} onCancel={cancel} okColor={props.okColor || defaults.value.okColor} okText={props.okText || defaults.value.okText} hideX hideHeader>
99
- <div class="flex space-x-4 py-2">
100
- <div class="flex-grow-0">
101
- <NIconCircle icon={props.icon || defaults.value.icon} iconSize={6} color={props.iconColor || defaults.value.iconColor}/>
102
- </div>
103
-
104
- <div class="flex-grow">
105
- <DialogTitle as="h4" class="font-medium text-lg text-default-700 mb-1">
106
- {props.title}
107
- </DialogTitle>
108
- {context.slots.default?.() || <p class="text-sm text-default-500">{props.text}</p>}
109
- </div>
110
- </div>
111
- </NModal>);
112
- });
113
- const VARIANT_DEFAULTS = {
114
- success: {
115
- icon: CheckIcon,
116
- iconColor: 'green',
117
- okText: trsl('vue-collection.action.all-right'),
118
- okColor: 'green',
119
- },
120
- info: {
121
- icon: LightBulbIcon,
122
- iconColor: 'blue',
123
- okText: trsl('vue-collection.action.all-right'),
124
- okColor: 'blue',
125
- },
126
- warning: {
127
- icon: ExclamationTriangleIcon,
128
- iconColor: 'yellow',
129
- okText: trsl('vue-collection.action.proceed'),
130
- okColor: 'yellow',
131
- },
132
- danger: {
133
- icon: ExclamationTriangleIcon,
134
- iconColor: 'red',
135
- okText: trsl('vue-collection.action.proceed'),
136
- okColor: 'red',
137
- },
138
- remove: {
139
- icon: TrashIcon,
140
- iconColor: 'red',
141
- okText: trsl('vue-collection.action.remove'),
142
- okColor: 'red',
143
- },
144
- };
@@ -1,92 +0,0 @@
1
- import { createComponent, createProps } from '../utils/component';
2
- import { computed, Transition } from 'vue';
3
- import { RouterLink } from 'vue-router';
4
- import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
5
- import { ChevronDownIcon } from '@heroicons/vue/24/solid';
6
- export const nDropdownProps = createProps({
7
- /**
8
- * The title of the dropdown-button.
9
- */
10
- title: String,
11
- /**
12
- * The items of the dropdown.
13
- * The second dimension of the array is used
14
- * to create groups of items, which are visually seperated.
15
- */
16
- items: {
17
- type: Array,
18
- default: () => [],
19
- },
20
- /**
21
- * If set to `true` the panel is right-aligned to the button.
22
- */
23
- right: Boolean,
24
- /**
25
- * If set to `true` the dropdown-button is disabled and no interaction is possible.
26
- */
27
- disabled: Boolean,
28
- /**
29
- * A slot to replace the button of the dropdown.
30
- */
31
- button: Function,
32
- });
33
- /**
34
- * The `NDropdown` consists of a button and a panel with multiple actions.
35
- * It is useful to group multiple actions together in one place.
36
- */
37
- export default createComponent('NDropdown', nDropdownProps, (props, { slots }) => {
38
- const items = computed(() => {
39
- if (props.items.length == 0)
40
- return [];
41
- if (Array.isArray(props.items[0]))
42
- return props.items;
43
- else
44
- return [props.items];
45
- });
46
- const itemWithIcon = (item) => (<div class="flex space-x-3 items-center">
47
- {item.icon && <item.icon class={['h-5 w-5', item.disabled ? 'text-default-300' : 'text-default-400']}/>}
48
- <span>{item.label}</span>
49
- </div>);
50
- return () => (<Menu as="div" class="relative inline-block text-left">
51
- <div class="flex">
52
- {props.button?.() || (<MenuButton disabled={props.disabled} class={[
53
- 'shadow w-full flex justify-between items-center text-default-700 rounded-md border bg-white border-default-300 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-offset-2 focus-visible:ring-2 focus-visible:ring-primary-500',
54
- props.disabled ? 'text-opacity-20 cursor-default' : 'hover:bg-default-100',
55
- ]}>
56
- <span>{props.title}</span>
57
- <ChevronDownIcon class="-mr-1 ml-2 h-5 w-5 flex-shrink-0" aria-hidden="true"/>
58
- </MenuButton>)}
59
- </div>
60
-
61
- <Transition enterActiveClass="transition ease-out duration-100" enterFromClass="transform opacity-0 scale-95" enterToClass="transform opacity-100 scale-100" leaveActiveClass="transition ease-in duration-75" leaveFromClass="transform opacity-100 scale-100" leaveToClass="transform opacity-0 scale-95">
62
- <MenuItems class={[
63
- 'z-10 absolute w-56 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
64
- props.right ? 'origin-top-right right-0' : 'origin-top-left left-0',
65
- ]}>
66
- {slots.default?.() || (<div class="divide-y divide-default-200">
67
- {items.value.map((group, index) => (<div key={`group-${index}`} class="py-1">
68
- {group.map((item, index) => (<MenuItem key={`item-${index}`} disabled={item.disabled}>
69
- {({ active }) => item.disabled ? (<div class="block px-4 py-2 text-sm text-default-300">
70
- {itemWithIcon(item)}
71
- </div>) : item.route ? (<RouterLink to={item.route} class={[
72
- 'block px-4 py-2 text-sm',
73
- active
74
- ? 'bg-default-100 text-default-900'
75
- : 'text-default-700',
76
- ]}>
77
- {itemWithIcon(item)}
78
- </RouterLink>) : (<button type="button" onClick={item.onClick} class={[
79
- 'w-full text-left px-4 py-2 text-sm',
80
- active
81
- ? 'bg-default-100 text-default-900'
82
- : 'text-default-700',
83
- ]}>
84
- {itemWithIcon(item)}
85
- </button>)}
86
- </MenuItem>))}
87
- </div>))}
88
- </div>)}
89
- </MenuItems>
90
- </Transition>
91
- </Menu>);
92
- });
@@ -1,211 +0,0 @@
1
- import { trslc } from '../i18n';
2
- import { createComponent, createProps } 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
- export const nDropzoneProps = createProps({
12
- ...vModelProps(Array),
13
- /**
14
- * A description which files are allowed for this dropzone.
15
- * This should include everything the user needs to know about
16
- * the file type / the extensions and the maximum size of the file.
17
- * @see {@link nDropzoneProps.accept}
18
- * @see {@link nDropzoneProps.maxFileSize}
19
- */
20
- description: String,
21
- /**
22
- * The maximum amount of files which can be added to the dropzone.
23
- */
24
- maxFiles: {
25
- type: Number,
26
- default: 1,
27
- },
28
- /**
29
- * Specifies which file types are accepted. The same syntax as with inputs of type file is used.
30
- * Make sure to explain the requirements to the user in the {@link nDropzoneProps.description}.
31
- * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
32
- */
33
- accept: String,
34
- /**
35
- * Specifies the maximum size of an individual file in bytes.
36
- * Make sure to explain the max size to the user in the {@link nDropzoneProps.description}.
37
- */
38
- maxFileSize: {
39
- type: Number,
40
- default: 100 * 1024 * 1024,
41
- },
42
- /**
43
- * The maximum length of the file names.
44
- * Files names longer than the specified amount of characters will be shortened.
45
- * @see {@link maxLengthSplitCenter}
46
- */
47
- maxLengthFileNames: {
48
- type: Number,
49
- default: 35,
50
- },
51
- /**
52
- * A tailwind height class, which is applied to the dropzone area.
53
- * It is recommended to use `min-h-*` classes,
54
- * so the box is always large enough to display it's text.
55
- */
56
- height: {
57
- type: String,
58
- default: 'min-h-36',
59
- },
60
- });
61
- /**
62
- * The `NDropzone` is an area where files can be added by the user by drag & drop.
63
- * Files can also be selected with a file chooser by clicking on the dropzone.
64
- */
65
- export default createComponent('NDropzone', nDropzoneProps, props => {
66
- const fileError = ref();
67
- const filterAndUpdateFiles = (files) => {
68
- // filter for mime type and max size
69
- const fileTypeFilteredFiles = files.filter(file => !props.accept || testFileWithAcceptString(props.accept, file));
70
- const filteredFiles = fileTypeFilteredFiles.filter(file => file.size <= props.maxFileSize);
71
- // filter for already existing files
72
- const currentFiles = props.value || [];
73
- filteredFiles.forEach(file => {
74
- if (currentFiles.filter(currFile => currFile.name == file.name).length == 0)
75
- currentFiles.push(file);
76
- });
77
- // slice down to max amount of files
78
- const newFiles = currentFiles.slice(0, props.maxFiles);
79
- // error handling
80
- const fileTypeFilterDiff = files.length - fileTypeFilteredFiles.length;
81
- const fileSizeFilterDiff = fileTypeFilteredFiles.length - filteredFiles.length;
82
- if (newFiles.length < currentFiles.length)
83
- fileError.value = trslc('vue-collection.error.too-many-files', props.maxFiles, { max: props.maxFiles });
84
- else if (fileSizeFilterDiff > 0) {
85
- fileError.value = trslc('vue-collection.error.file-size', fileSizeFilterDiff, { n: fileSizeFilterDiff });
86
- }
87
- else if (fileTypeFilterDiff > 0)
88
- fileError.value = trslc('vue-collection.error.file-type', fileTypeFilterDiff, { n: fileTypeFilterDiff });
89
- else
90
- fileError.value = undefined;
91
- // update new value
92
- props.onUpdateValue?.(newFiles);
93
- };
94
- const files = computed(() => props.value?.map((file, index) => ({
95
- index,
96
- name: maxLengthSplitCenter(file.name, props.maxLengthFileNames),
97
- })) || []);
98
- const removeFile = (index) => {
99
- const newFiles = [...(props.value || [])];
100
- newFiles.splice(index, 1);
101
- fileError.value = undefined;
102
- props.onUpdateValue?.(newFiles);
103
- };
104
- const clearFiles = () => {
105
- fileError.value = undefined;
106
- props.onUpdateValue?.([]);
107
- };
108
- const isDragOver = ref(false);
109
- const onDrop = (event) => {
110
- event.preventDefault();
111
- isDragOver.value = false;
112
- const transfer = event.dataTransfer;
113
- if (transfer == null)
114
- return;
115
- if (transfer.items.length > 0) {
116
- const items = [...transfer.items];
117
- const files = items.map(item => (item.kind === 'file' ? item.getAsFile() : null)).filter(notNullish);
118
- filterAndUpdateFiles(files);
119
- }
120
- else {
121
- filterAndUpdateFiles([...transfer.files]);
122
- }
123
- };
124
- const onDragOver = (event) => {
125
- event.preventDefault();
126
- isDragOver.value = true;
127
- };
128
- const onDragLeave = () => {
129
- isDragOver.value = false;
130
- };
131
- const onClick = () => {
132
- fileInput.value?.click();
133
- };
134
- const fileInput = ref();
135
- const onInputFilesChanged = () => {
136
- const input = fileInput.value;
137
- if (input != null) {
138
- if (input.files != null)
139
- filterAndUpdateFiles([...input.files]);
140
- input.value = '';
141
- }
142
- };
143
- return () => (<div>
144
- <button class={[
145
- 'block w-full rounded-md border-dashed border-2 hover:border-primary-300 hover:bg-primary-50 focus-visible:border-primary-500 focus:outline-none ',
146
- 'flex flex-col items-center justify-center text-center text-sm select-none hover:text-primary-700 p-4',
147
- isDragOver.value
148
- ? 'border-primary-300 bg-primary-50 text-primary-700'
149
- : 'border-default-300 bg-default-50 text-default-500',
150
- props.height,
151
- ]} onDrop={onDrop} onDragover={onDragOver} onDragleave={onDragLeave} onClick={onClick}>
152
- <input type="file" class="hidden" ref={fileInput} multiple={props.maxFiles > 1} accept={props.accept} onChange={onInputFilesChanged} onClick={event => event.stopPropagation()}/>
153
-
154
- {/* Counterweight */}
155
- <div class="flex-grow mb-2"/>
156
-
157
- <span class="font-medium">
158
- {trslc('vue-collection.text.drag-n-drop-files', props.maxFiles, { n: props.maxFiles })}
159
- </span>
160
- <span>{props.description}</span>
161
-
162
- <div class="flex-grow mt-2 flex items-end justify-center text-red-500 font-medium">
163
- <span>{fileError.value}</span>
164
- </div>
165
- </button>
166
-
167
- <div class="mt-2 space-y-1">
168
- <div class="flex flex-wrap gap-2 ">
169
- {files.value.map(file => (<NBadge key={file.index} color="default" allCaps={false} textSize="text-xs">
170
- <div class="flex items-center space-x-2">
171
- <span>{file.name}</span>
172
- <NIconButton icon={XMarkIcon} shade={900} size={3} onClick={() => removeFile(file.index)}/>
173
- </div>
174
- </NBadge>))}
175
-
176
- <div class="flex-grow text-sm text-default-500 flex items-end justify-end text-right">
177
- <span>
178
- <span>
179
- {trslc('vue-collection.text.files-selected', files.value.length, {
180
- n: files.value.length,
181
- })}
182
- </span>
183
- {files.value.length > 0 && (<>
184
- <span> </span>
185
- <NLink color="default" onClick={clearFiles}>
186
- {trslc('vue-collection.action.clear-files', files.value.length)}
187
- </NLink>
188
- </>)}
189
- </span>
190
- </div>
191
- </div>
192
- </div>
193
- </div>);
194
- });
195
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
196
- const MIME_FORMAT = /^(image|audio|application|video|text)\/\*$/;
197
- const EXTENSION_FORMAT = /^\.\w{2,20}$/;
198
- function testFileWithAcceptString(accept, file) {
199
- const splitted = accept.split(',').map(pattern => pattern.trim());
200
- for (const pattern of splitted) {
201
- if (MIME_FORMAT.test(pattern)) {
202
- if (RegExp(`^${pattern.substring(0, pattern.length - 2)}\\/.{2,}$`).test(file.type))
203
- return true;
204
- }
205
- else if (EXTENSION_FORMAT.test(pattern)) {
206
- if (RegExp(`^.*${pattern}$`).test(file.name))
207
- return true;
208
- }
209
- }
210
- return false;
211
- }