@datametria/vue-components 2.2.0 → 2.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.
Files changed (78) hide show
  1. package/README.md +25 -7
  2. package/dist/index.es.js +3378 -2148
  3. package/dist/index.umd.js +9 -9
  4. package/dist/src/components/DatametriaAutocomplete.vue.d.ts +14 -17
  5. package/dist/src/components/DatametriaBreadcrumb.vue.d.ts +39 -7
  6. package/dist/src/components/DatametriaCheckbox.vue.d.ts +35 -6
  7. package/dist/src/components/DatametriaCheckboxGroup.vue.d.ts +30 -0
  8. package/dist/src/components/DatametriaDataTable.vue.d.ts +64 -0
  9. package/dist/src/components/DatametriaDatePicker.vue.d.ts +15 -37
  10. package/dist/src/components/DatametriaDialog.vue.d.ts +71 -0
  11. package/dist/src/components/DatametriaEmpty.vue.d.ts +30 -0
  12. package/dist/src/components/DatametriaFloatingBar.vue.d.ts +2 -2
  13. package/dist/src/components/DatametriaForm.vue.d.ts +40 -0
  14. package/dist/src/components/DatametriaFormItem.vue.d.ts +28 -0
  15. package/dist/src/components/DatametriaGrid.vue.d.ts +1 -1
  16. package/dist/src/components/DatametriaInput.vue.d.ts +69 -10
  17. package/dist/src/components/DatametriaMenu.vue.d.ts +3 -3
  18. package/dist/src/components/DatametriaNavbar.vue.d.ts +2 -2
  19. package/dist/src/components/DatametriaPagination.vue.d.ts +29 -0
  20. package/dist/src/components/DatametriaPopconfirm.vue.d.ts +43 -0
  21. package/dist/src/components/DatametriaProgress.vue.d.ts +33 -8
  22. package/dist/src/components/DatametriaRadio.vue.d.ts +25 -6
  23. package/dist/src/components/DatametriaRadioGroup.vue.d.ts +29 -0
  24. package/dist/src/components/DatametriaResult.vue.d.ts +30 -0
  25. package/dist/src/components/DatametriaSelect.vue.d.ts +16 -11
  26. package/dist/src/components/DatametriaSidebar.vue.d.ts +3 -3
  27. package/dist/src/components/DatametriaSlider.vue.d.ts +3 -3
  28. package/dist/src/components/DatametriaSortableTable.vue.d.ts +1 -1
  29. package/dist/src/components/DatametriaSteps.vue.d.ts +45 -0
  30. package/dist/src/components/DatametriaSwitch.vue.d.ts +9 -4
  31. package/dist/src/components/DatametriaTabPane.vue.d.ts +28 -0
  32. package/dist/src/components/DatametriaTextarea.vue.d.ts +27 -8
  33. package/dist/src/components/DatametriaTimePicker.vue.d.ts +17 -25
  34. package/dist/src/components/DatametriaToast.vue.d.ts +1 -1
  35. package/dist/src/components/DatametriaTooltip.vue.d.ts +1 -1
  36. package/dist/src/components/DatametriaTree.vue.d.ts +31 -0
  37. package/dist/src/components/DatametriaTreeNode.vue.d.ts +17 -0
  38. package/dist/src/components/DatametriaUpload.vue.d.ts +64 -0
  39. package/dist/src/index.d.ts +14 -0
  40. package/dist/vue-components.css +1 -1
  41. package/package.json +4 -3
  42. package/src/components/DatametriaAutocomplete.vue +155 -260
  43. package/src/components/DatametriaBreadcrumb.vue +66 -80
  44. package/src/components/DatametriaCheckbox.vue +150 -37
  45. package/src/components/DatametriaCheckboxGroup.vue +43 -0
  46. package/src/components/DatametriaDataTable.vue +304 -0
  47. package/src/components/DatametriaDatePicker.vue +238 -614
  48. package/src/components/DatametriaDialog.vue +295 -0
  49. package/src/components/DatametriaDropdown.vue +352 -0
  50. package/src/components/DatametriaEmpty.vue +153 -0
  51. package/src/components/DatametriaForm.vue +160 -0
  52. package/src/components/DatametriaFormItem.vue +181 -0
  53. package/src/components/DatametriaInput.vue +226 -63
  54. package/src/components/DatametriaPagination.vue +373 -0
  55. package/src/components/DatametriaPopconfirm.vue +236 -0
  56. package/src/components/DatametriaProgress.vue +176 -63
  57. package/src/components/DatametriaRadio.vue +83 -72
  58. package/src/components/DatametriaRadioGroup.vue +42 -0
  59. package/src/components/DatametriaResult.vue +133 -0
  60. package/src/components/DatametriaSelect.vue +172 -67
  61. package/src/components/DatametriaSortableTable.vue +35 -4
  62. package/src/components/DatametriaSteps.vue +314 -0
  63. package/src/components/DatametriaSwitch.vue +86 -80
  64. package/src/components/DatametriaTabPane.vue +82 -0
  65. package/src/components/DatametriaTextarea.vue +140 -100
  66. package/src/components/DatametriaTimePicker.vue +231 -214
  67. package/src/components/DatametriaTree.vue +124 -0
  68. package/src/components/DatametriaTreeNode.vue +174 -0
  69. package/src/components/DatametriaUpload.vue +365 -0
  70. package/src/index.ts +25 -11
  71. package/src/components/__tests__/DatametriaAutocomplete.test.ts +0 -180
  72. package/src/components/__tests__/DatametriaBreadcrumb.test.ts +0 -75
  73. package/src/components/__tests__/DatametriaCheckbox.test.ts +0 -47
  74. package/src/components/__tests__/DatametriaDatePicker.test.ts +0 -234
  75. package/src/components/__tests__/DatametriaProgress.test.ts +0 -90
  76. package/src/components/__tests__/DatametriaRadio.test.ts +0 -77
  77. package/src/components/__tests__/DatametriaSwitch.test.ts +0 -64
  78. package/src/components/__tests__/DatametriaTextarea.test.ts +0 -66
@@ -1,141 +1,103 @@
1
1
  <template>
2
- <div class="dm-autocomplete" ref="autocompleteRef">
3
- <label v-if="label" :for="inputId" class="dm-autocomplete__label">
4
- {{ label }}
5
- <span v-if="required" class="dm-autocomplete__required">*</span>
6
- </label>
7
- <div class="dm-autocomplete__wrapper">
8
- <input
9
- :id="inputId"
10
- v-model="searchQuery"
11
- type="text"
12
- role="combobox"
13
- class="dm-autocomplete__input"
14
- :class="{ 'dm-autocomplete__input--error': error }"
15
- :placeholder="placeholder"
16
- :disabled="disabled"
17
- :required="required"
18
- :aria-label="ariaLabel"
19
- :aria-expanded="isOpen"
20
- :aria-controls="`${inputId}-listbox`"
21
- :aria-activedescendant="activeOptionId"
22
- @focus="handleFocus"
23
- @blur="handleBlur"
24
- @keydown="handleKeydown"
25
- @input="handleInput"
26
- />
27
- <div v-if="loading" class="dm-autocomplete__loading">
28
- <span class="dm-autocomplete__spinner"></span>
29
- </div>
30
- </div>
31
-
32
- <div
33
- v-if="isOpen"
34
- :id="`${inputId}-listbox`"
35
- class="dm-autocomplete__dropdown"
36
- role="listbox"
37
- >
38
- <div
39
- v-if="filteredOptions.length === 0"
40
- class="dm-autocomplete__no-results"
41
- >
42
- No results found
43
- </div>
2
+ <div class="datametria-autocomplete" :class="{ 'datametria-autocomplete--disabled': disabled }">
3
+ <input
4
+ ref="inputRef"
5
+ v-model="inputValue"
6
+ type="text"
7
+ class="datametria-autocomplete__input"
8
+ :placeholder="placeholder"
9
+ :disabled="disabled"
10
+ @input="handleInput"
11
+ @focus="handleFocus"
12
+ @blur="handleBlur"
13
+ @keydown.down.prevent="selectNext"
14
+ @keydown.up.prevent="selectPrev"
15
+ @keydown.enter.prevent="selectCurrent"
16
+ />
17
+ <Teleport to="body">
44
18
  <div
45
- v-for="(option, index) in filteredOptions"
46
- :key="option.value"
47
- :id="`${inputId}-option-${index}`"
48
- class="dm-autocomplete__option"
49
- :class="{ 'dm-autocomplete__option--active': index === activeIndex }"
50
- role="option"
51
- :aria-selected="index === activeIndex"
52
- @click="selectOption(option)"
53
- @mouseenter="activeIndex = index"
19
+ v-if="isOpen && suggestions.length"
20
+ ref="dropdownRef"
21
+ class="datametria-autocomplete__dropdown"
22
+ :style="dropdownStyle"
54
23
  >
55
- {{ option.label }}
56
- </div>
57
- </div>
58
-
59
- <div v-if="multiple && selectedOptions.length > 0" class="dm-autocomplete__tags">
60
- <span
61
- v-for="option in selectedOptions"
62
- :key="option.value"
63
- class="dm-autocomplete__tag"
64
- >
65
- {{ option.label }}
66
- <button
67
- type="button"
68
- class="dm-autocomplete__tag-remove"
69
- @click="removeOption(option)"
24
+ <div
25
+ v-for="(item, index) in suggestions"
26
+ :key="index"
27
+ class="datametria-autocomplete__item"
28
+ :class="{ 'datametria-autocomplete__item--active': index === activeIndex }"
29
+ @mousedown.prevent="selectItem(item)"
30
+ @mouseenter="activeIndex = index"
70
31
  >
71
- ×
72
- </button>
73
- </span>
74
- </div>
75
-
76
- <div v-if="error" class="dm-autocomplete__error">
77
- {{ error }}
78
- </div>
32
+ <span v-html="highlightMatch(item)"></span>
33
+ </div>
34
+ </div>
35
+ </Teleport>
79
36
  </div>
80
37
  </template>
81
38
 
82
39
  <script setup lang="ts">
83
- import { ref, computed, onMounted, onUnmounted } from 'vue'
84
-
85
- interface Option {
86
- value: string | number
87
- label: string
88
- [key: string]: any
89
- }
40
+ import { ref, computed, watch } from 'vue'
90
41
 
91
42
  interface Props {
92
- modelValue?: string | number | Option | Option[]
93
- options: (Option | string)[]
43
+ modelValue?: string
44
+ fetchSuggestions: (query: string) => Promise<string[]> | string[]
45
+ debounce?: number
46
+ triggerOnFocus?: boolean
94
47
  placeholder?: string
95
- label?: string
96
- error?: string
97
48
  disabled?: boolean
98
- required?: boolean
99
- loading?: boolean
100
- multiple?: boolean
101
- ariaLabel?: string
102
49
  }
103
50
 
104
51
  const props = withDefaults(defineProps<Props>(), {
105
- placeholder: 'Search...',
106
- multiple: false
52
+ modelValue: '',
53
+ debounce: 300,
54
+ triggerOnFocus: false,
55
+ placeholder: '',
56
+ disabled: false
107
57
  })
108
58
 
109
59
  const emit = defineEmits<{
110
- 'update:modelValue': [value: string | number | Option | Option[]]
60
+ 'update:modelValue': [value: string]
61
+ select: [value: string]
111
62
  }>()
112
63
 
113
- const autocompleteRef = ref<HTMLElement>()
114
- const searchQuery = ref('')
64
+ const inputRef = ref<HTMLInputElement>()
65
+ const dropdownRef = ref<HTMLDivElement>()
66
+ const inputValue = ref(props.modelValue)
67
+ const suggestions = ref<string[]>([])
115
68
  const isOpen = ref(false)
116
69
  const activeIndex = ref(-1)
117
- const selectedOptions = ref<Option[]>([])
118
-
119
- const inputId = computed(() => `autocomplete-${Math.random().toString(36).substr(2, 9)}`)
120
- const activeOptionId = computed(() =>
121
- activeIndex.value >= 0 ? `${inputId.value}-option-${activeIndex.value}` : undefined
122
- )
70
+ const loading = ref(false)
71
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
72
+
73
+ const dropdownStyle = computed(() => {
74
+ if (!inputRef.value) return {}
75
+ const rect = inputRef.value.getBoundingClientRect()
76
+ return {
77
+ position: 'fixed' as const,
78
+ top: `${rect.bottom + 4}px`,
79
+ left: `${rect.left}px`,
80
+ width: `${rect.width}px`,
81
+ zIndex: 9999
82
+ }
83
+ })
123
84
 
124
- const filteredOptions = computed(() => {
125
- const normalizedOptions = props.options.map(option =>
126
- typeof option === 'string'
127
- ? { value: option, label: option }
128
- : option
129
- )
85
+ const handleInput = () => {
86
+ emit('update:modelValue', inputValue.value)
130
87
 
131
- if (!searchQuery.value) return normalizedOptions
132
- return normalizedOptions.filter(option =>
133
- option.label.toLowerCase().includes(searchQuery.value.toLowerCase())
134
- )
135
- })
88
+ if (debounceTimer) {
89
+ clearTimeout(debounceTimer)
90
+ }
91
+
92
+ debounceTimer = setTimeout(() => {
93
+ fetchSuggestionsData()
94
+ }, props.debounce)
95
+ }
136
96
 
137
97
  const handleFocus = () => {
138
- isOpen.value = true
98
+ if (props.triggerOnFocus && inputValue.value) {
99
+ fetchSuggestionsData()
100
+ }
139
101
  }
140
102
 
141
103
  const handleBlur = () => {
@@ -145,194 +107,127 @@ const handleBlur = () => {
145
107
  }, 200)
146
108
  }
147
109
 
148
- const handleInput = () => {
149
- isOpen.value = true
110
+ const fetchSuggestionsData = async () => {
111
+ if (!inputValue.value) {
112
+ suggestions.value = []
113
+ isOpen.value = false
114
+ return
115
+ }
116
+
117
+ loading.value = true
118
+ try {
119
+ const result = await props.fetchSuggestions(inputValue.value)
120
+ suggestions.value = result
121
+ isOpen.value = result.length > 0
122
+ activeIndex.value = -1
123
+ } catch (error) {
124
+ suggestions.value = []
125
+ isOpen.value = false
126
+ } finally {
127
+ loading.value = false
128
+ }
129
+ }
130
+
131
+ const selectItem = (item: string) => {
132
+ inputValue.value = item
133
+ emit('update:modelValue', item)
134
+ emit('select', item)
135
+ isOpen.value = false
150
136
  activeIndex.value = -1
151
137
  }
152
138
 
153
- const handleKeydown = (event: KeyboardEvent) => {
154
- switch (event.key) {
155
- case 'ArrowDown':
156
- event.preventDefault()
157
- if (activeIndex.value < filteredOptions.value.length - 1) {
158
- activeIndex.value++
159
- }
160
- break
161
- case 'ArrowUp':
162
- event.preventDefault()
163
- if (activeIndex.value > 0) {
164
- activeIndex.value--
165
- }
166
- break
167
- case 'Enter':
168
- event.preventDefault()
169
- if (activeIndex.value >= 0) {
170
- selectOption(filteredOptions.value[activeIndex.value])
171
- }
172
- break
173
- case 'Escape':
174
- isOpen.value = false
175
- activeIndex.value = -1
176
- break
139
+ const selectNext = () => {
140
+ if (activeIndex.value < suggestions.value.length - 1) {
141
+ activeIndex.value++
177
142
  }
178
143
  }
179
144
 
180
- const selectOption = (option: Option) => {
181
- if (props.multiple) {
182
- if (!selectedOptions.value.find(o => o.value === option.value)) {
183
- selectedOptions.value.push(option)
184
- emit('update:modelValue', selectedOptions.value)
185
- }
186
- } else {
187
- searchQuery.value = option.label
188
- emit('update:modelValue', option)
189
- isOpen.value = false
145
+ const selectPrev = () => {
146
+ if (activeIndex.value > 0) {
147
+ activeIndex.value--
190
148
  }
191
149
  }
192
150
 
193
- const removeOption = (option: Option) => {
194
- selectedOptions.value = selectedOptions.value.filter(o => o.value !== option.value)
195
- emit('update:modelValue', selectedOptions.value)
151
+ const selectCurrent = () => {
152
+ if (activeIndex.value >= 0 && activeIndex.value < suggestions.value.length) {
153
+ selectItem(suggestions.value[activeIndex.value])
154
+ }
196
155
  }
197
156
 
198
- const handleClickOutside = (event: Event) => {
199
- if (autocompleteRef.value && !autocompleteRef.value.contains(event.target as Node)) {
200
- isOpen.value = false
201
- }
157
+ const highlightMatch = (text: string): string => {
158
+ if (!inputValue.value) return text
159
+
160
+ const regex = new RegExp(`(${escapeRegex(inputValue.value)})`, 'gi')
161
+ return text.replace(regex, '<strong>$1</strong>')
202
162
  }
203
163
 
204
- onMounted(() => {
205
- document.addEventListener('click', handleClickOutside)
206
- })
164
+ const escapeRegex = (str: string): string => {
165
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
166
+ }
207
167
 
208
- onUnmounted(() => {
209
- document.removeEventListener('click', handleClickOutside)
168
+ watch(() => props.modelValue, (newValue) => {
169
+ if (newValue !== inputValue.value) {
170
+ inputValue.value = newValue
171
+ }
210
172
  })
211
173
  </script>
212
174
 
213
175
  <style scoped>
214
- .dm-autocomplete {
176
+ .datametria-autocomplete {
215
177
  position: relative;
178
+ display: inline-block;
216
179
  width: 100%;
217
180
  }
218
181
 
219
- .dm-autocomplete__label {
220
- display: block;
221
- margin-bottom: 0.5rem;
222
- font-weight: 500;
223
- color: var(--dm-neutral-700, #374151);
224
- }
225
-
226
- .dm-autocomplete__required {
227
- color: var(--dm-error, #ef4444);
228
- }
229
-
230
- .dm-autocomplete__wrapper {
231
- position: relative;
232
- }
233
-
234
- .dm-autocomplete__input {
182
+ .datametria-autocomplete__input {
235
183
  width: 100%;
236
- padding: 0.75rem;
237
- border: 1px solid var(--dm-neutral-300, #d1d5db);
238
- border-radius: var(--dm-radius-md, 0.375rem);
239
- font-size: 1rem;
184
+ padding: 8px 12px;
185
+ border: 1px solid var(--datametria-border-color, #dcdfe6);
186
+ border-radius: 4px;
187
+ font-size: 14px;
188
+ color: var(--datametria-text-color, #303133);
189
+ background-color: var(--datametria-bg-color, #ffffff);
240
190
  transition: border-color 0.2s;
241
191
  }
242
192
 
243
- .dm-autocomplete__input:focus {
244
- outline: none;
245
- border-color: var(--dm-primary, #0072ce);
246
- box-shadow: 0 0 0 3px rgba(0, 114, 206, 0.1);
247
- }
248
-
249
- .dm-autocomplete__input--error {
250
- border-color: var(--dm-error, #ef4444);
251
- }
252
-
253
- .dm-autocomplete__loading {
254
- position: absolute;
255
- right: 0.75rem;
256
- top: 50%;
257
- transform: translateY(-50%);
193
+ .datametria-autocomplete__input:hover {
194
+ border-color: var(--datametria-border-color-hover, #c0c4cc);
258
195
  }
259
196
 
260
- .dm-autocomplete__spinner {
261
- display: inline-block;
262
- width: 1rem;
263
- height: 1rem;
264
- border: 2px solid var(--dm-neutral-300, #d1d5db);
265
- border-top: 2px solid var(--dm-primary, #0072ce);
266
- border-radius: 50%;
267
- animation: spin 1s linear infinite;
197
+ .datametria-autocomplete__input:focus {
198
+ outline: none;
199
+ border-color: var(--datametria-primary-color, #0072ce);
268
200
  }
269
201
 
270
- @keyframes spin {
271
- 0% { transform: rotate(0deg); }
272
- 100% { transform: rotate(360deg); }
202
+ .datametria-autocomplete--disabled .datametria-autocomplete__input {
203
+ background-color: var(--datametria-disabled-bg-color, #f5f7fa);
204
+ cursor: not-allowed;
273
205
  }
274
206
 
275
- .dm-autocomplete__dropdown {
276
- position: absolute;
277
- top: 100%;
278
- left: 0;
279
- right: 0;
280
- z-index: 1000;
281
- background: white;
282
- border: 1px solid var(--dm-neutral-300, #d1d5db);
283
- border-radius: var(--dm-radius-md, 0.375rem);
284
- box-shadow: var(--dm-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
207
+ .datametria-autocomplete__dropdown {
208
+ background: var(--datametria-bg-color, #ffffff);
209
+ border: 1px solid var(--datametria-border-color, #dcdfe6);
210
+ border-radius: 4px;
211
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
285
212
  max-height: 200px;
286
213
  overflow-y: auto;
287
214
  }
288
215
 
289
- .dm-autocomplete__option {
290
- padding: 0.75rem;
216
+ .datametria-autocomplete__item {
217
+ padding: 8px 12px;
291
218
  cursor: pointer;
219
+ font-size: 14px;
220
+ color: var(--datametria-text-color, #303133);
292
221
  transition: background-color 0.2s;
293
222
  }
294
223
 
295
- .dm-autocomplete__option:hover,
296
- .dm-autocomplete__option--active {
297
- background-color: var(--dm-neutral-100, #f3f4f6);
298
- }
299
-
300
- .dm-autocomplete__no-results {
301
- padding: 0.75rem;
302
- color: var(--dm-neutral-500, #6b7280);
303
- text-align: center;
304
- }
305
-
306
- .dm-autocomplete__tags {
307
- display: flex;
308
- flex-wrap: wrap;
309
- gap: 0.5rem;
310
- margin-top: 0.5rem;
311
- }
312
-
313
- .dm-autocomplete__tag {
314
- display: inline-flex;
315
- align-items: center;
316
- gap: 0.25rem;
317
- padding: 0.25rem 0.5rem;
318
- background-color: var(--dm-primary, #0072ce);
319
- color: white;
320
- border-radius: var(--dm-radius-sm, 0.25rem);
321
- font-size: 0.875rem;
322
- }
323
-
324
- .dm-autocomplete__tag-remove {
325
- background: none;
326
- border: none;
327
- color: white;
328
- cursor: pointer;
329
- font-size: 1.25rem;
330
- line-height: 1;
224
+ .datametria-autocomplete__item:hover,
225
+ .datametria-autocomplete__item--active {
226
+ background-color: var(--datametria-hover-bg-color, #f5f7fa);
331
227
  }
332
228
 
333
- .dm-autocomplete__error {
334
- margin-top: 0.25rem;
335
- color: var(--dm-error, #ef4444);
336
- font-size: 0.875rem;
229
+ .datametria-autocomplete__item :deep(strong) {
230
+ color: var(--datametria-primary-color, #0072ce);
231
+ font-weight: 600;
337
232
  }
338
- </style>
233
+ </style>