@opengis/cms 0.0.21 → 0.0.22

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 (183) hide show
  1. package/package.json +2 -2
  2. package/src/App.css +52 -0
  3. package/src/App.vue +62 -0
  4. package/src/assets/image.png +0 -0
  5. package/src/assets/main.css +3 -0
  6. package/src/assets/tailwind-3.4.17.js +113 -0
  7. package/src/components/LanguageSwitcher.vue +73 -0
  8. package/src/components/SettingsCard.vue +40 -0
  9. package/src/components/builder/CreateForm.vue +128 -0
  10. package/src/components/builder/formTypeSchema.js +145 -0
  11. package/src/components/builder/tabs/index.ts +9 -0
  12. package/src/components/builder/tabs/vs-builder-edit.vue +133 -0
  13. package/src/components/builder/tabs/vs-builder-monaco.vue +29 -0
  14. package/src/components/builder/tabs/vs-builder-preview.vue +39 -0
  15. package/src/components/builder/vs-builder-datatable-controls.vue +138 -0
  16. package/src/components/builder/vs-builder-datatable-form.vue +80 -0
  17. package/src/components/builder/vs-builder-datatable.vue +191 -0
  18. package/src/components/builder/vs-builder-list-item.vue +110 -0
  19. package/src/components/collections/CollectionsBreadcrumb.vue +52 -0
  20. package/src/components/collections/CollectionsGrid.vue +176 -0
  21. package/src/components/collections/ContentBlock.vue +75 -0
  22. package/src/components/collections/formWrapper.vue +156 -0
  23. package/src/components/dashboard/ContentItem.vue +82 -0
  24. package/src/components/dashboard/DashboardHeader.vue +33 -0
  25. package/src/components/dashboard/QuickActions.vue +84 -0
  26. package/src/components/dashboard/RecentContent.vue +54 -0
  27. package/src/components/dashboard/StatCard.vue +63 -0
  28. package/src/components/dashboard/StatsGrid.vue +28 -0
  29. package/src/components/form-components/MonacoEditor.vue +104 -0
  30. package/src/components/form-components/VsFormMeta.vue +40 -0
  31. package/src/components/form-components/VsFormTags.vue +150 -0
  32. package/src/components/form-components/custom-datatable/vs-form-custom-datatable-add.vue +84 -0
  33. package/src/components/form-components/custom-datatable/vs-form-custom-datatable-controls.vue +106 -0
  34. package/src/components/form-components/index.js +23 -0
  35. package/src/components/form-components/reference/vs-form-reference-add.vue +92 -0
  36. package/src/components/form-components/reference/vs-form-reference-controls.vue +101 -0
  37. package/src/components/form-components/reference-list/referenceOptionList.js +78 -0
  38. package/src/components/form-components/reference-list/vs-form-reference-add.vue +145 -0
  39. package/src/components/form-components/reference-list/vs-form-reference-choce.vue +39 -0
  40. package/src/components/form-components/reference-list/vs-form-reference-controls.vue +110 -0
  41. package/src/components/form-components/reference-skeleton/about-skeleton.vue +37 -0
  42. package/src/components/form-components/reference-skeleton/banner-skeleton.vue +29 -0
  43. package/src/components/form-components/reference-skeleton/body-skeleton.vue +56 -0
  44. package/src/components/form-components/reference-skeleton/cards-skeleton.vue +47 -0
  45. package/src/components/form-components/reference-skeleton/documents-skeleton.vue +64 -0
  46. package/src/components/form-components/reference-skeleton/faq-skeleton.vue +64 -0
  47. package/src/components/form-components/reference-skeleton/form-skeleton.vue +41 -0
  48. package/src/components/form-components/reference-skeleton/index.js +36 -0
  49. package/src/components/form-components/reference-skeleton/infoLine-skeleton.vue +37 -0
  50. package/src/components/form-components/reference-skeleton/news-skeleton.vue +54 -0
  51. package/src/components/form-components/reference-skeleton/slider-skeleton.vue +41 -0
  52. package/src/components/form-components/reference-skeleton/tabs-skeleton.vue +40 -0
  53. package/src/components/form-components/reference-skeleton/team-skeleton.vue +103 -0
  54. package/src/components/form-components/reference-skeleton/usefulLinks-skeleton.vue +52 -0
  55. package/src/components/form-components/reference-skeleton/video-skeleton.vue +36 -0
  56. package/src/components/form-components/testReferenceTypes.js +773 -0
  57. package/src/components/form-components/vs-form-color-picker.vue +29 -0
  58. package/src/components/form-components/vs-form-custom-datatable.vue +214 -0
  59. package/src/components/form-components/vs-form-integer.vue +86 -0
  60. package/src/components/form-components/vs-form-key-value.vue +201 -0
  61. package/src/components/form-components/vs-form-marcdown-md.vue +3 -0
  62. package/src/components/form-components/vs-form-media-select.vue +780 -0
  63. package/src/components/form-components/vs-form-reference-list.vue +97 -0
  64. package/src/components/form-components/vs-form-reference.vue +59 -0
  65. package/src/components/form-components/vs-form-relation.vue +30 -0
  66. package/src/components/form-components/vs-form-reletion-link.vue +34 -0
  67. package/src/components/form-components/vs-form-select-collection.vue +0 -0
  68. package/src/components/form-components/vs-form-slug.vue +72 -0
  69. package/src/components/form-components/vs-form-tiptap.vue +7 -0
  70. package/src/components/form-components/vs-richtext-md.vue +3 -0
  71. package/src/components/icons/BellIcon.vue +17 -0
  72. package/src/components/icons/GlobeIcon.vue +18 -0
  73. package/src/components/icons/KeyIcon.vue +20 -0
  74. package/src/components/icons/PaletteIcon.vue +22 -0
  75. package/src/components/icons/SettingsIcon.vue +19 -0
  76. package/src/components/icons/ShieldIcon.vue +18 -0
  77. package/src/components/icons/UsersIcon.vue +19 -0
  78. package/src/components/icons/icon-chevron-right.vue +16 -0
  79. package/src/components/icons/icon-drag.vue +20 -0
  80. package/src/components/icons/icon-file-text.vue +21 -0
  81. package/src/components/icons/icon-folder.vue +18 -0
  82. package/src/components/icons/icon-grid.vue +17 -0
  83. package/src/components/icons/icon-group.vue +19 -0
  84. package/src/components/icons/icon-home.vue +16 -0
  85. package/src/components/icons/icon-image.vue +18 -0
  86. package/src/components/icons/icon-list.vue +20 -0
  87. package/src/components/icons/icon-more.vue +17 -0
  88. package/src/components/icons/icon-plus.vue +17 -0
  89. package/src/components/icons-types/icon-array.vue +22 -0
  90. package/src/components/icons-types/icon-boolean.vue +18 -0
  91. package/src/components/icons-types/icon-datalist.vue +22 -0
  92. package/src/components/icons-types/icon-date.vue +20 -0
  93. package/src/components/icons-types/icon-datetime.vue +20 -0
  94. package/src/components/icons-types/icon-file.vue +21 -0
  95. package/src/components/icons-types/icon-gallery.vue +18 -0
  96. package/src/components/icons-types/icon-image.vue +19 -0
  97. package/src/components/icons-types/icon-integer.vue +20 -0
  98. package/src/components/icons-types/icon-merkdown.vue +18 -0
  99. package/src/components/icons-types/icon-multiselect.vue +22 -0
  100. package/src/components/icons-types/icon-number.vue +20 -0
  101. package/src/components/icons-types/icon-radio.vue +22 -0
  102. package/src/components/icons-types/icon-reference-list.vue +22 -0
  103. package/src/components/icons-types/icon-reference.vue +20 -0
  104. package/src/components/icons-types/icon-relation.vue +22 -0
  105. package/src/components/icons-types/icon-richtext.vue +18 -0
  106. package/src/components/icons-types/icon-select.vue +22 -0
  107. package/src/components/icons-types/icon-slug.vue +19 -0
  108. package/src/components/icons-types/icon-text.vue +19 -0
  109. package/src/components/icons-types/index.js +43 -0
  110. package/src/components/layout/Layout.vue +67 -0
  111. package/src/components/layout/Sidebar.vue +128 -0
  112. package/src/components/media/FileUploadProgress.vue +29 -0
  113. package/src/components/media/MediaBreadcrumb.vue +42 -0
  114. package/src/components/media/MediaCreateFolder.vue +59 -0
  115. package/src/components/media/MediaFileInfo.vue +148 -0
  116. package/src/components/media/MediaGrid.vue +148 -0
  117. package/src/components/media/MediaList.vue +148 -0
  118. package/src/components/media/MediaViewControls.vue +38 -0
  119. package/src/components/media/TypeTag.vue +23 -0
  120. package/src/components/menu/AddNewItemInTree.vue +75 -0
  121. package/src/components/menu/MenuBody.vue +149 -0
  122. package/src/components/menu/MenuItem.vue +73 -0
  123. package/src/components/menu/MenuList.vue +101 -0
  124. package/src/components/referencec/index.ts +7 -0
  125. package/src/components/referencec/vs-reference-faq.vue +61 -0
  126. package/src/components/referencec/vs-reference-user-card.vue +40 -0
  127. package/src/components/settings/NotificationSettings.vue +32 -0
  128. package/src/components/settings/SettingsTable.vue +50 -0
  129. package/src/components/settings/SettingsTitle.vue +33 -0
  130. package/src/components/settings/SettingsToggleItem.vue +25 -0
  131. package/src/components/sidebar/DropdownMenu.vue +34 -0
  132. package/src/components/sidebar/SettingsSidebar.vue +121 -0
  133. package/src/components/sidebar/SidebarFooter.vue +52 -0
  134. package/src/components/sidebar/SidebarHeader.vue +57 -0
  135. package/src/components/sidebar/SidebarMenu.vue +78 -0
  136. package/src/components/ui/EmptyData.vue +76 -0
  137. package/src/components/ui/UniversalTable.vue +310 -0
  138. package/src/components/ui/UniversalTableFilters.vue +0 -0
  139. package/src/components/ui/UniversalTablePagination.vue +118 -0
  140. package/src/components/ui/VsPreview.vue +75 -0
  141. package/src/composables/useCollectionView.ts +21 -0
  142. package/src/composables/useDebounce.ts +26 -0
  143. package/src/composables/useMonaco.ts +28 -0
  144. package/src/composables/useTheme.ts +40 -0
  145. package/src/content/test-slug/metadata.json +1 -0
  146. package/src/i18n.ts +75 -0
  147. package/src/index.css +3 -0
  148. package/src/locales/en.json +778 -0
  149. package/src/locales/uk.json +797 -0
  150. package/src/main.ts +41 -0
  151. package/src/pages/Dashboard.vue +168 -0
  152. package/src/pages/EmailPage.vue +183 -0
  153. package/src/pages/FeedbackPage.vue +232 -0
  154. package/src/pages/MediaPage.vue +372 -0
  155. package/src/pages/TagsPage.vue +207 -0
  156. package/src/pages/builder/BuilderPage.vue +195 -0
  157. package/src/pages/builder/EditCollectionPage.vue +163 -0
  158. package/src/pages/collections/ArticlesPage.vue +385 -0
  159. package/src/pages/collections/CollectionsPage.vue +146 -0
  160. package/src/pages/collections/SingletonsPage.vue +119 -0
  161. package/src/pages/collections/contentForm.vue +484 -0
  162. package/src/pages/collections/schema/seo.ts +27 -0
  163. package/src/pages/menu/MenuAddPage.vue +123 -0
  164. package/src/pages/menu/MenuItemPage.vue +183 -0
  165. package/src/pages/menu/MenuPage.vue +133 -0
  166. package/src/pages/settings/ApiKeys.vue +75 -0
  167. package/src/pages/settings/Appearance.vue +80 -0
  168. package/src/pages/settings/Logs.vue +260 -0
  169. package/src/pages/settings/PermissionsPage.vue +237 -0
  170. package/src/pages/settings/Settings.vue +186 -0
  171. package/src/pages/settings/Users.vue +109 -0
  172. package/src/pages/settings/general.vue +154 -0
  173. package/src/pages/settings/generalScheme.js +132 -0
  174. package/src/pages/users/AddUser.vue +106 -0
  175. package/src/pages/users/UsersPage.vue +98 -0
  176. package/src/props/builder.ts +67 -0
  177. package/src/props/content.ts +56 -0
  178. package/src/props/media.ts +63 -0
  179. package/src/router/index.ts +181 -0
  180. package/src/types/fastify-auth.d.ts +4 -0
  181. package/src/utils/getField.js +270 -0
  182. package/src/utils/translit.js +19 -0
  183. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <input
3
+ type="color"
4
+ v-model="modelValueLocal"
5
+ class="w-8 h-8 p-0 border border-gray-300 rounded cursor-pointer bg-transparent"
6
+ @input="onInput"
7
+ />
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { defineProps, defineEmits, computed } from 'vue';
12
+
13
+ const props = defineProps<{
14
+ modelValue: string
15
+ }>();
16
+ const emit = defineEmits<{
17
+ (e: 'update:modelValue', value: string): void
18
+ }>();
19
+
20
+ const modelValueLocal = computed({
21
+ get: () => props.modelValue,
22
+ set: (val: string) => emit('update:modelValue', val)
23
+ });
24
+
25
+ function onInput(e: Event) {
26
+ const value = (e.target as HTMLInputElement).value;
27
+ emit('update:modelValue', value);
28
+ }
29
+ </script>
@@ -0,0 +1,214 @@
1
+ <template>
2
+ <div class="p-0 shadow w-full">
3
+ <div class="overflow-x-auto">
4
+ <div class="relative w-full overflow-auto">
5
+ <table class="w-full caption-bottom text-sm">
6
+ <thead class="[&amp;_tr]:border-b">
7
+ <tr
8
+ class="border-b data-[state=selected]:bg-muted bg-slate-50/50 dark:bg-slate-700/50 hover:bg-slate-50/80 dark:hover:bg-slate-700/80 transition-colors"
9
+ >
10
+ <th
11
+ class="h-10 px-2 align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;&gt;[role=checkbox]]:translate-y-[2px] w-12 text-center"
12
+ ></th>
13
+ <th
14
+ v-for="field in colModel"
15
+ :key="field.name"
16
+ class="h-10 px-2 text-left align-middle [&amp;:has([role=checkbox])]:pr-0 [&amp;&gt;[role=checkbox]]:translate-y-[2px] font-semibold text-slate-700 dark:text-slate-300 min-w-[150px]"
17
+ >
18
+ {{ field.label || field.title || field.name }}
19
+ </th>
20
+
21
+ <th
22
+ class="h-10 px-2 align-middle [&amp;:has([role=checkbox])]:pr-0 [&amp;&gt;[role=checkbox]]:translate-y-[2px] font-semibold text-slate-700 dark:text-slate-300 text-end"
23
+ >
24
+ {{ $t('table.actions') }}
25
+ </th>
26
+ </tr>
27
+ </thead>
28
+ <tbody class="[&amp;_tr:last-child]:border-0">
29
+ <tr
30
+ v-for="(item, index) in data"
31
+ :key="item?.id"
32
+ class="border-b data-[state=selected]:bg-muted transition-all duration-200 hover:bg-slate-50/60 dark:hover:bg-slate-700/60 cursor-move"
33
+ :class="{ 'opacity-50': draggedItem === item }"
34
+ draggable="true"
35
+ @dragstart="handleDragStart($event, item, index)"
36
+ @dragover="handleDragOver($event, index)"
37
+ @drop="handleDrop($event, index)"
38
+ @dragenter="handleDragEnter($event, index)"
39
+ @dragleave="handleDragLeave($event)"
40
+ >
41
+ <td
42
+ class="p-2 align-middle [&amp;:has([role=checkbox])]:pr-0 [&amp;&gt;[role=checkbox]]:translate-y-[2px] text-center"
43
+ >
44
+ <div class="flex items-center justify-center text-slate-400">
45
+ <GripVertical />
46
+ </div>
47
+ </td>
48
+
49
+ <td
50
+ v-for="field in colModel"
51
+ :key="field.name"
52
+ class="p-2 align-middle [&amp;:has([role=checkbox])]:pr-0 [&amp;&gt;[role=checkbox]]:translate-y-[2px] text-slate-600 dark:text-slate-400 text-sm"
53
+ >
54
+ <span v-if="field.type === 'Texteditor'" v-html="item[field.key] || '-'"></span>
55
+ <div v-else-if="field.type === 'File'" class="flex items-center gap-2">
56
+ <div v-if="Array.isArray(item[field.key]) && item[field.key].length > 0" class="flex items-center gap-1">
57
+ <div
58
+ v-for="(file, fileIndex) in getDisplayFiles(item[field.key])"
59
+ :key="fileIndex"
60
+ class="relative w-8 h-8 rounded overflow-hidden bg-gray-100 dark:bg-gray-700 flex-shrink-0"
61
+ >
62
+ <img
63
+ :src="file"
64
+ :alt="`Image ${fileIndex + 1}`"
65
+ class="w-full h-full object-cover"
66
+ @error="handleImageError($event)"
67
+ />
68
+ </div>
69
+ <span v-if="getRemainingFilesCount(item[field.key]) > 0" class="text-xs text-gray-500 dark:text-gray-400 ml-1">
70
+ +{{ getRemainingFilesCount(item[field.key]) }}
71
+ </span>
72
+ </div>
73
+ <div v-else class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
74
+ <svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
75
+ <path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd" />
76
+ </svg>
77
+ </div>
78
+ </div>
79
+ <span v-else>{{ item[field.key] || "-" }}</span>
80
+ </td>
81
+
82
+ <td
83
+ class="p-2 align-middle [&amp;:has([role=checkbox])]:pr-0 [&amp;&gt;[role=checkbox]]:translate-y-[2px]"
84
+ >
85
+ <VsFormCustomDatatableControls
86
+ :item="item"
87
+ :colModel="colModel"
88
+ v-model="data"
89
+ />
90
+ </td>
91
+ </tr>
92
+
93
+ <!-- Drop zone indicator -->
94
+ <tr
95
+ v-if="showDropZone"
96
+ class="h-2 bg-blue-200 dark:bg-blue-800 transition-all duration-200"
97
+ ></tr>
98
+ </tbody>
99
+ </table>
100
+ </div>
101
+ </div>
102
+ <VsFormCustomDatatableAdd :colModel="colModel" v-model="data" />
103
+ </div>
104
+ </template>
105
+
106
+ <script setup>
107
+ import { GripVertical } from "lucide-vue-next";
108
+ import VsFormCustomDatatableControls from "./custom-datatable/vs-form-custom-datatable-controls.vue";
109
+ import VsFormCustomDatatableAdd from "./custom-datatable/vs-form-custom-datatable-add.vue";
110
+ import { onMounted, ref } from "vue";
111
+
112
+ const props = defineProps({
113
+ colModel: {
114
+ type: Array,
115
+ required: true,
116
+ },
117
+ });
118
+
119
+ const data = defineModel({
120
+ type: Array,
121
+ required: true,
122
+ });
123
+
124
+ // Drag and drop state
125
+ const draggedItem = ref(null);
126
+ const draggedIndex = ref(-1);
127
+ const showDropZone = ref(false);
128
+
129
+ // Drag and drop handlers
130
+ const handleDragStart = (event, item, index) => {
131
+ draggedItem.value = item;
132
+ draggedIndex.value = index;
133
+ event.dataTransfer.effectAllowed = "move";
134
+ event.dataTransfer.setData("text/html", ""); // Required for Firefox
135
+ };
136
+
137
+ const handleDragOver = (event, index) => {
138
+ event.preventDefault();
139
+ event.dataTransfer.dropEffect = "move";
140
+ };
141
+
142
+ const handleDragEnter = (event, index) => {
143
+ if (draggedIndex.value !== index) {
144
+ showDropZone.value = true;
145
+ }
146
+ };
147
+
148
+ const handleDragLeave = (event) => {
149
+ // Only hide drop zone if we're leaving the table entirely
150
+ if (!event.currentTarget.contains(event.relatedTarget)) {
151
+ showDropZone.value = false;
152
+ }
153
+ };
154
+
155
+ const handleDrop = (event, dropIndex) => {
156
+ event.preventDefault();
157
+ showDropZone.value = false;
158
+
159
+ if (draggedIndex.value === -1 || draggedIndex.value === dropIndex) {
160
+ return;
161
+ }
162
+
163
+ // Reorder the array
164
+ const newData = [...data.value];
165
+ const [removed] = newData.splice(draggedIndex.value, 1);
166
+ newData.splice(dropIndex, 0, removed);
167
+
168
+ // Update the model
169
+ data.value = newData;
170
+
171
+ // Reset drag state
172
+ draggedItem.value = null;
173
+ draggedIndex.value = -1;
174
+ };
175
+
176
+ // Constants for file display
177
+ const MAX_DISPLAY_FILES = 3;
178
+
179
+ // Get files to display (limited to MAX_DISPLAY_FILES)
180
+ const getDisplayFiles = (files) => {
181
+ if (!Array.isArray(files)) return [];
182
+ return files.slice(0, MAX_DISPLAY_FILES);
183
+ };
184
+
185
+ // Get count of remaining files not displayed
186
+ const getRemainingFilesCount = (files) => {
187
+ if (!Array.isArray(files)) return 0;
188
+ return Math.max(0, files.length - MAX_DISPLAY_FILES);
189
+ };
190
+
191
+ // Handle image loading errors
192
+ const handleImageError = (event) => {
193
+ const img = event.target;
194
+ img.style.display = 'none';
195
+ const placeholder = document.createElement('div');
196
+ placeholder.className = 'w-full h-full flex items-center justify-center bg-gray-200 dark:bg-gray-600';
197
+ placeholder.innerHTML = `
198
+ <svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
199
+ <path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd" />
200
+ </svg>
201
+ `;
202
+ img.parentNode.appendChild(placeholder);
203
+ };
204
+
205
+ onMounted(() => {
206
+ data.value
207
+ ?.filter((el) => typeof el === "object")
208
+ ?.forEach((item) =>
209
+ item.id
210
+ ? item.id
211
+ : (item.id = Math.random().toString(36).substring(2, 15))
212
+ );
213
+ });
214
+ </script>
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div class="p-0">
3
+ <slot name="label" v-bind="{ id }"></slot>
4
+ <input
5
+ v-model="inputValue"
6
+ type="number"
7
+ @keydown="validateInput"
8
+ @blur="handleBlur"
9
+ :step="step"
10
+ :min="min"
11
+ :max="max"
12
+ :id="id"
13
+ aria-describedby="helper-text-explanation"
14
+ class="vs-number__input py-2 px-3 block w-full h-[38px] border border-solid border-stone-200 rounded-lg text-sm text-stone-800 placeholder:text-stone-500 focus:z-10 focus:border-1 focus:outline-none focus:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-500 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-neutral-600"
15
+ :placeholder="placeholder"
16
+ :disabled="disabled"
17
+ :class="{ 'cursor-not-allowed': disabled }"
18
+ />
19
+ </div>
20
+ </template>
21
+ <script>
22
+ export default {
23
+ props: {
24
+ min: { type: Number },
25
+ step: { type: Number, default: () => 1 },
26
+ max: { type: Number, default: () => Infinity },
27
+ modelValue: { type: Number, default: () => null },
28
+ label: { type: String, default: () => "" },
29
+ placeholder: { type: String, default: () => "0" },
30
+ disabled: { type: Boolean, default: () => false },
31
+ },
32
+ data() {
33
+ return {
34
+ id: `input-number-${Math.floor(Math.random() * 1000)}`,
35
+ inputValue: this.modelValue,
36
+ };
37
+ },
38
+ watch: {
39
+ modelValue(newVal) {
40
+ this.inputValue = newVal;
41
+ },
42
+ inputValue(newVal) {
43
+ this.$emit("update:modelValue", newVal);
44
+ },
45
+ },
46
+ methods: {
47
+ validateInput(event) {
48
+ if (
49
+ event.key === "+" ||
50
+ event.key === "," ||
51
+ event.key === "." ||
52
+ (event.key === "-" &&
53
+ this.inputValue &&
54
+ this.inputValue.toString().length > 0)
55
+ ) {
56
+ event.preventDefault();
57
+ }
58
+ },
59
+ handleBlur() {
60
+ let value = parseFloat(this.inputValue);
61
+
62
+ this.$emit("blur");
63
+
64
+ if (isNaN(value)) {
65
+ value = this.min;
66
+ } else if (value > this.max) {
67
+ value = this.max;
68
+ } else if (value < this.min) {
69
+ value = this.min;
70
+ }
71
+ this.inputValue = value;
72
+ this.$emit("update:modelValue", this.inputValue);
73
+ },
74
+ },
75
+ };
76
+ </script>
77
+
78
+ <style>
79
+ .col-error .vs-number__input {
80
+ border: 1px solid red;
81
+ }
82
+
83
+ .vs-number__input::placeholder {
84
+ opacity: 0.5;
85
+ }
86
+ </style>
@@ -0,0 +1,201 @@
1
+ <template>
2
+ <div class="w-full">
3
+ <div ref="sortableContainer" class="flex flex-col gap-1">
4
+ <div
5
+ v-for="item in value"
6
+ :key="item?.id"
7
+ class="flex items-center gap-2 w-full"
8
+ >
9
+ <button class="drag-handle cursor-move">
10
+ <svg
11
+ fill="#000000"
12
+ width="20"
13
+ height="20"
14
+ viewBox="0 0 36 36"
15
+ version="1.1"
16
+ preserveAspectRatio="xMidYMid meet"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ xmlns:xlink="http://www.w3.org/1999/xlink"
19
+ >
20
+ <title>drag-handle-line</title>
21
+ <circle
22
+ cx="15"
23
+ cy="12"
24
+ r="1.5"
25
+ class="clr-i-outline clr-i-outline-path-1"
26
+ ></circle>
27
+ <circle
28
+ cx="15"
29
+ cy="24"
30
+ r="1.5"
31
+ class="clr-i-outline clr-i-outline-path-2"
32
+ ></circle>
33
+ <circle
34
+ cx="21"
35
+ cy="12"
36
+ r="1.5"
37
+ class="clr-i-outline clr-i-outline-path-3"
38
+ ></circle>
39
+ <circle
40
+ cx="21"
41
+ cy="24"
42
+ r="1.5"
43
+ class="clr-i-outline clr-i-outline-path-4"
44
+ ></circle>
45
+ <circle
46
+ cx="21"
47
+ cy="18"
48
+ r="1.5"
49
+ class="clr-i-outline clr-i-outline-path-5"
50
+ ></circle>
51
+ <circle
52
+ cx="15"
53
+ cy="18"
54
+ r="1.5"
55
+ class="clr-i-outline clr-i-outline-path-6"
56
+ ></circle>
57
+ <rect x="0" y="0" width="36" height="36" fill-opacity="0" />
58
+ </svg>
59
+ </button>
60
+
61
+ <!-- Key field -->
62
+ <div class="w-1/3">
63
+ <VsText v-model="item.key" type="text" :placeholder="t('form.key')" />
64
+ </div>
65
+
66
+ <!-- Value field -->
67
+ <div class="w-2/3">
68
+ <VsText v-model="item.value" type="text" :placeholder="t('form.value')" />
69
+ </div>
70
+
71
+ <button
72
+ @click="deleteItem(item?.id)"
73
+ class="shrink-0 w-[30px] text-gray-600 h-[30px] rounded-full flex items-center justify-center bg-gray-100 duration-300 hover:bg-gray-200"
74
+ >
75
+ <X height="16" width="16" />
76
+ </button>
77
+ </div>
78
+ </div>
79
+
80
+ <span v-if="!value.length" class="text-[14px] text-gray-700"
81
+ >{{ t('form.noDataToDisplay') }}</span
82
+ >
83
+ <div class="mt-2 w-full flex items-center">
84
+ <button
85
+ type="button"
86
+ @click="addItem"
87
+ :disabled="disabled"
88
+ class="py-1.5 px-2 flex items-center gap-x-1 text-xs font-medium rounded-full border border-dashed border-gray-200 bg-white text-gray-800 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-hidden focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700"
89
+ >
90
+ <Plus height="14" width="14" /> {{ t('form.add') }}
91
+ </button>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <script setup>
97
+ import { computed, ref, watch, onMounted, inject } from "vue";
98
+ import { Plus, X } from "lucide-vue-next";
99
+ import Sortable from "sortablejs";
100
+ import { useI18n } from 'vue-i18n';
101
+ const { t } = useI18n();
102
+
103
+ const getRandomId = (prefix) => {
104
+ return `${prefix}-${Math.random().toString(36).substring(2, 15)}`;
105
+ };
106
+
107
+ const props = defineProps({
108
+ colModel: { type: Object, default: () => ({ type: "Text" }) },
109
+ ignore: { type: Array, default: () => [] },
110
+ addButtonText: { type: String, default: () => "Додати" },
111
+ });
112
+
113
+ const metaParentValue = inject("metaParentValue");
114
+
115
+ // Convert object to array of key-value pairs, исключая игнорируемые ключи
116
+ const value = ref(
117
+ Object.entries(metaParentValue.value || {})
118
+ .filter(([key]) => !props.ignore.includes(key))
119
+ .map(([key, val]) => ({
120
+ id: getRandomId("key-value-"),
121
+ key: key,
122
+ value: val,
123
+ }))
124
+ );
125
+
126
+ const sortableContainer = ref(null);
127
+
128
+ onMounted(() => {
129
+ if (sortableContainer.value) {
130
+ new Sortable(sortableContainer.value, {
131
+ handle: ".drag-handle",
132
+ animation: 150,
133
+ ghostClass: "sortable-ghost",
134
+ onEnd: ({ oldIndex, newIndex }) => {
135
+ if (oldIndex !== newIndex) {
136
+ const item = value.value.splice(oldIndex, 1)[0];
137
+ value.value.splice(newIndex, 0, item);
138
+ }
139
+ },
140
+ });
141
+ }
142
+ });
143
+
144
+ const disabled = computed(() => {
145
+ // Отключаем кнопку только если есть незавершенные записи
146
+ return value.value?.some(
147
+ (i) => (i?.key && !i?.value) || (!i?.key && i?.value)
148
+ );
149
+ });
150
+
151
+ const addItem = () => {
152
+ value.value.push({ id: getRandomId("key-value-"), key: "", value: "" });
153
+ };
154
+
155
+ const deleteItem = (id) => {
156
+ value.value = value.value?.filter((i) => i?.id !== id);
157
+ };
158
+
159
+ watch(
160
+ value,
161
+ (n) => {
162
+ // Convert array back to object
163
+ const result = {};
164
+ n.forEach((item) => {
165
+ if (item.key && item.value !== undefined) {
166
+ result[item.key] = item.value;
167
+ }
168
+ });
169
+ const tempValue = {};
170
+ if (props.ignore.length) {
171
+ props.ignore.forEach((item) => {
172
+ tempValue[item] = metaParentValue.value[item];
173
+ });
174
+ }
175
+
176
+ // Сохраняем игнорируемые поля и добавляем новые/измененные
177
+ metaParentValue.value = {
178
+ ...tempValue,
179
+ ...result,
180
+ };
181
+ },
182
+ { deep: true }
183
+ );
184
+ </script>
185
+
186
+ <style scoped>
187
+ .list-enter-active,
188
+ .list-leave-active {
189
+ transition: all 0.3s ease;
190
+ }
191
+ .list-enter-from,
192
+ .list-leave-to {
193
+ opacity: 0;
194
+ transform: translateY(10px);
195
+ }
196
+
197
+ .sortable-ghost {
198
+ opacity: 0.5;
199
+ background: #f0f0f0;
200
+ }
201
+ </style>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <vs-richtext-md v-bind="$attrs" />
3
+ </template>