@naptics/vue-collection 0.0.2 → 0.0.4

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