mediacube-ui 0.1.347 → 0.1.349

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 (107) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/mediacube-ui.common.js +384 -0
  3. package/package.json +11 -4
  4. package/src/elements/McAvatar/McAvatar.vue +276 -0
  5. package/src/elements/McBadge/McBadge.vue +153 -0
  6. package/src/elements/McButton/McButton.vue +850 -0
  7. package/src/elements/McChip/McChip.vue +305 -0
  8. package/src/elements/McCropper/McCropper.vue +135 -0
  9. package/src/elements/McDate/McDate.vue +107 -0
  10. package/src/elements/McDatePicker/McDatePicker.vue +910 -0
  11. package/src/elements/McField/McFieldCheckbox/McFieldCheckbox.vue +339 -0
  12. package/src/elements/McField/McFieldRadio/McFieldRadioButton/McFieldRadioButton.vue +207 -0
  13. package/src/elements/McField/McFieldRadio/McFieldRadioGroup/McFieldRadioGroup.vue +198 -0
  14. package/src/elements/McField/McFieldSelect/McFieldSelect.vue +1088 -0
  15. package/src/elements/McField/McFieldText/McFieldText.vue +977 -0
  16. package/src/elements/McField/McFieldToggle/McFieldToggle.vue +273 -0
  17. package/src/elements/McInfinityLoadingIndicator/McInfinityLoadingIndicator.vue +97 -0
  18. package/src/elements/McNotification/McNotification.vue +215 -0
  19. package/src/elements/McProgress/McProgress.vue +223 -0
  20. package/src/elements/McRangeSlider/McRangeSlider.vue +199 -0
  21. package/src/elements/McSeparator/McSeparator.vue +143 -0
  22. package/src/elements/McSlideUpDown/McSlideUpDown.vue +157 -0
  23. package/src/elements/McSvgIcon/McSvgIcon.vue +128 -0
  24. package/src/elements/McTabs/McTab/McTab.vue +189 -0
  25. package/src/elements/McTabs/McTabs/McTabs.vue +538 -0
  26. package/src/elements/McTitle/McTitle.vue +370 -0
  27. package/src/elements/McTooltip/McTooltip.vue +339 -0
  28. package/src/helpers/consts.js +3 -0
  29. package/src/helpers/delayedAction.js +26 -0
  30. package/src/helpers/storybookFunctions.js +19 -0
  31. package/src/helpers/storybookVariables.js +24 -0
  32. package/src/mixins/equalFieldHeight.js +59 -0
  33. package/src/mixins/fieldErrors.js +28 -0
  34. package/src/patterns/McAccordion/McAccordion.vue +53 -0
  35. package/src/patterns/McCells/McCell/McCell.vue +102 -0
  36. package/src/patterns/McChat/McChat.vue +306 -0
  37. package/src/patterns/McChat/McChatComment/McChatComment.vue +268 -0
  38. package/src/patterns/McChat/McChatForm/McChatForm.vue +150 -0
  39. package/src/patterns/McCollapse/McCollapse.vue +282 -0
  40. package/src/patterns/McDrawer/McDrawer.vue +150 -0
  41. package/src/patterns/McDropdown/McDropdown.vue +249 -0
  42. package/src/patterns/McDropdown/McDropdownPanel/McDropdownPanel.vue +44 -0
  43. package/src/patterns/McFakeScroll/McFakeScroll.vue +279 -0
  44. package/src/patterns/McFilter/McFilter.vue +847 -0
  45. package/src/patterns/McFilter/McFilterChip/McFilterChip.vue +85 -0
  46. package/src/patterns/McFilter/McFilterTags/McFilterTags.vue +376 -0
  47. package/src/patterns/McFilter/McFilterTypeDate/McFilterTypeDate.vue +70 -0
  48. package/src/patterns/McFilter/McFilterTypeRange/McFilterTypeRange.vue +133 -0
  49. package/src/patterns/McFilter/McFilterTypeRelation/McFilterTypeRelation.vue +224 -0
  50. package/src/patterns/McFilter/McFilterTypeSimple/McFilterTypeSimple.vue +164 -0
  51. package/src/patterns/McFilter/McFilterTypeText/McFilterTypeText.vue +62 -0
  52. package/src/patterns/McGrid/McGridCol/McGridCol.vue +166 -0
  53. package/src/patterns/McGrid/McGridRow/McGridRow.vue +158 -0
  54. package/src/patterns/McModal/McModal.vue +686 -0
  55. package/src/patterns/McOverlay/McOverlay.vue +79 -0
  56. package/src/patterns/McPreview/McPreview.vue +119 -0
  57. package/src/patterns/McSideBar/McSideBar/McSideBar.vue +389 -0
  58. package/src/patterns/McSideBar/McSideBarBottom/McSideBarBottom.vue +126 -0
  59. package/src/patterns/McSideBar/McSideBarButton/McSideBarButton.vue +257 -0
  60. package/src/patterns/McSideBar/McSideBarCenter/McSideBarCenter.vue +369 -0
  61. package/src/patterns/McSideBar/McSideBarTop/McSideBarTop.vue +243 -0
  62. package/src/patterns/McStack/McStack.vue +163 -0
  63. package/src/patterns/McTable/McTable/McTable.vue +860 -0
  64. package/src/patterns/McTable/McTableCol/McTableCol.vue +299 -0
  65. package/src/patterns/McTableCard/McTableCard.vue +138 -0
  66. package/src/patterns/McTableCard/McTableCardHeader/McTableCardHeader.vue +76 -0
  67. package/src/patterns/McTopBar/McTopBar.vue +155 -0
  68. package/src/patterns/McWrapScroll/McWrapScroll.vue +293 -0
  69. package/src/styles/_functions.scss +189 -0
  70. package/src/styles/_mixins.scss +619 -0
  71. package/src/styles/_spacing.scss +33 -0
  72. package/src/styles/_variables.scss +23 -0
  73. package/src/styles/global.scss +311 -0
  74. package/src/styles/main.scss +4 -0
  75. package/src/styles/table.scss +12 -0
  76. package/src/styles/toast.scss +59 -0
  77. package/src/templates/layouts/McContentFixed/McContentFixed.vue +60 -0
  78. package/src/templates/layouts/McMain/McMain.vue +115 -0
  79. package/src/templates/layouts/McRoot/McRoot.vue +45 -0
  80. package/src/tokens/animations.scss +9 -0
  81. package/src/tokens/border-radius.scss +26 -0
  82. package/src/tokens/box-shadows.scss +28 -0
  83. package/src/tokens/colors.scss +82 -0
  84. package/src/tokens/durations.scss +7 -0
  85. package/src/tokens/easings.scss +6 -0
  86. package/src/tokens/font-families.scss +8 -0
  87. package/src/tokens/font-sizes.scss +23 -0
  88. package/src/tokens/font-weights.scss +9 -0
  89. package/src/tokens/gradients.scss +19 -0
  90. package/src/tokens/letter-spacings.scss +6 -0
  91. package/src/tokens/line-heights.scss +22 -0
  92. package/src/tokens/media-queries.scss +32 -0
  93. package/src/tokens/opacities.scss +8 -0
  94. package/src/tokens/sizes.scss +47 -0
  95. package/src/tokens/spacings.scss +38 -0
  96. package/src/tokens/z-indexes.scss +14 -0
  97. package/src/utils/dayjs.js +19 -0
  98. package/src/utils/filters.js +11 -0
  99. package/src/utils/getTokens.js +41 -0
  100. package/src/utils/load-icons.js +3 -0
  101. package/src/utils/treeSearch.js +30 -0
  102. package/src/utils/webFontLoader.js +12 -0
  103. package/dist/0.mediacube-ui.umd.js +0 -50
  104. package/dist/assets/img/icons.3b7d59b2f49c67a2a3a4566b8ab233fd.svg +0 -1
  105. package/dist/assets/img/no_table_data.236cd56f46cfb71fc363b008d4ca70d5.png +0 -0
  106. package/dist/assets/img/no_user.e0030d6e54e2400e1181fd22b83cf8ae.png +0 -0
  107. package/dist/mediacube-ui.umd.js +0 -1
@@ -0,0 +1,1088 @@
1
+ <template>
2
+ <div :ref="field_key" :dir="dir" :class="classes" :style="styles">
3
+ <div :for="name" class="mc-field-select__header">
4
+ <!-- @slot Слот заголовка -->
5
+ <slot name="header">
6
+ <mc-title v-if="hasTitle" :ellipsis="false" max-width="100%" weight="medium">
7
+ {{ computedTitle }}
8
+ </mc-title>
9
+ </slot>
10
+ </div>
11
+ <div class="mc-field-select__main">
12
+ <multi-select
13
+ :ref="key"
14
+ v-bind="tagBind"
15
+ @input="handleChange"
16
+ @tag="handleTag"
17
+ @search-change="handleSearchChange"
18
+ @open="handleOpen"
19
+ @close="handleClose"
20
+ >
21
+ <template v-if="isShowLimitToggle" slot="caret">
22
+ <div :class="computedCaretClass" @click="toggleOptions" />
23
+ </template>
24
+ <template v-if="collapsed && !is_show_all_options" slot="limit">
25
+ <mc-title variation="body" class="mc-field-select__limit-text">
26
+ {{ limitText }}
27
+ </mc-title>
28
+ </template>
29
+ <template slot="singleLabel" slot-scope="{ option }">
30
+ <mc-preview v-if="optionWithPreview" class="option__desc" size="l">
31
+ <mc-svg-icon slot="left" :name="option.icon" size="400" />
32
+ <mc-title slot="top" weight="semi-bold" v-html="option.name" />
33
+ <!-- Слот для замены стандартной стрелки при выведенном превью -->
34
+ <slot slot="right" name="arrow" />
35
+ <mc-title slot="bottom" color="gray">
36
+ {{ option.text }}
37
+ </mc-title>
38
+ </mc-preview>
39
+ <div v-else class="mc-field-select__single-label">
40
+ <div v-if="hasPrepend" class="mc-field-select__prepend">
41
+ <mc-avatar v-if="avatar" :src="avatar" />
42
+ <mc-svg-icon v-else :name="icon" />
43
+ </div>
44
+ <div
45
+ class="mc-field-select__label-text"
46
+ :class="hasPrepend ? 'mc-field-select__label-text--indent-left' : ''"
47
+ >
48
+ {{ option ? option.name : placeholder }}
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <template v-if="optionsTooltip || optionWithPreview" slot="option" slot-scope="{ option }">
54
+ <mc-preview v-if="optionWithPreview" class="option__desc" size="l">
55
+ <mc-svg-icon slot="left" :name="option.icon" size="400" />
56
+ <mc-title slot="top" weight="semi-bold" v-html="option.name" />
57
+ <mc-title slot="bottom" color="gray">
58
+ {{ option.text }}
59
+ </mc-title>
60
+ </mc-preview>
61
+ <mc-tooltip
62
+ v-else
63
+ class="mc-field-select__options-tooltip-target"
64
+ max-width="m"
65
+ color="black"
66
+ placement="top"
67
+ :content="option.name"
68
+ >
69
+ <span>{{ option.name }}</span>
70
+ </mc-tooltip>
71
+ </template>
72
+ <!-- @slot Слот для текста, если ничего не найдено -->
73
+ <slot slot="noResult" name="noResult">
74
+ <span>{{ noResultsText }}</span>
75
+ </slot>
76
+ </multi-select>
77
+ </div>
78
+ <div v-if="errorText || helpText || $slots.footer" class="mc-field-select__footer">
79
+ <mc-title
80
+ v-if="errorText"
81
+ tag-name="div"
82
+ color="red"
83
+ variation="overline"
84
+ max-width="100%"
85
+ :ellipsis="false"
86
+ >
87
+ {{ errorText }}
88
+ </mc-title>
89
+ <br v-if="errorText" />
90
+ <!-- @slot Слот доп. текста под инпутом -->
91
+ <slot name="footer">
92
+ <mc-title
93
+ v-if="helpText"
94
+ tag-name="div"
95
+ variation="overline"
96
+ color="gray"
97
+ max-width="100%"
98
+ :ellipsis="false"
99
+ >
100
+ {{ helpText }}
101
+ </mc-title>
102
+ </slot>
103
+ </div>
104
+ </div>
105
+ </template>
106
+
107
+ <script>
108
+ import MultiSelect from 'vue-multiselect'
109
+ import McTitle from '../../McTitle/McTitle'
110
+ import McTooltip from '../../McTooltip/McTooltip'
111
+ import McAvatar from '../../McAvatar/McAvatar'
112
+ import McSvgIcon from '../../McSvgIcon/McSvgIcon'
113
+ import McPreview from '../../../patterns/McPreview/McPreview'
114
+ import fieldErrors from '../../../mixins/fieldErrors'
115
+ import equalFieldHeight from '../../../mixins/equalFieldHeight'
116
+ import { LANGUAGES } from '../../../helpers/consts'
117
+ export default {
118
+ name: 'McFieldSelect',
119
+ components: { McSvgIcon, McAvatar, McTitle, McTooltip, MultiSelect, McPreview },
120
+ mixins: [fieldErrors, equalFieldHeight],
121
+ props: {
122
+ /**
123
+ * Заголовок поля:
124
+ *
125
+ */
126
+ title: {
127
+ type: String,
128
+ default: null,
129
+ },
130
+
131
+ /**
132
+ * Вспомогательный текст под инпутом:
133
+ *
134
+ */
135
+ helpText: {
136
+ type: String,
137
+ default: null,
138
+ },
139
+ /**
140
+ * Массив элементов
141
+ * выпадающего списка
142
+ * [
143
+ * {
144
+ * name: String,
145
+ * value: String | Number,
146
+ * text: String - доступен, если optionWithPreview=true
147
+ * icon: String - доступен, если optionWithPreview=true
148
+ * }
149
+ * ]
150
+ */
151
+ options: {
152
+ type: Array,
153
+ required: true,
154
+ },
155
+ /**
156
+ * Выполняется ли поиск из списка
157
+ * при вводе в инпут
158
+ */
159
+ searchable: {
160
+ type: Boolean,
161
+ default: true,
162
+ },
163
+ /**
164
+ * Множественный выбор
165
+ */
166
+ multiple: {
167
+ type: Boolean,
168
+ default: false,
169
+ },
170
+ /**
171
+ * Скрывать из списка
172
+ * выбранные элементы
173
+ */
174
+ hideSelected: {
175
+ type: Boolean,
176
+ default: true,
177
+ },
178
+ /**
179
+ * Допустимо ли
180
+ * пустое значение
181
+ */
182
+ allowEmpty: {
183
+ type: Boolean,
184
+ default: false,
185
+ },
186
+ /**
187
+ * Отключенное состояние
188
+ */
189
+ disabled: {
190
+ type: Boolean,
191
+ default: false,
192
+ },
193
+ /**
194
+ * Ссылка на аватар/картинку
195
+ * в начале label
196
+ */
197
+ avatar: {
198
+ type: String,
199
+ default: null,
200
+ },
201
+ /**
202
+ * Имя иконки
203
+ * в начале label
204
+ */
205
+ icon: {
206
+ type: String,
207
+ default: null,
208
+ },
209
+ /**
210
+ * Цвет фона
211
+ */
212
+ backgroundColor: {
213
+ type: String,
214
+ default: null,
215
+ },
216
+ /**
217
+ * placeholder
218
+ */
219
+ placeholder: {
220
+ type: String,
221
+ default: '',
222
+ },
223
+ /**
224
+ * Направление открытия списка:
225
+ * `above (top), below (bottom), auto`
226
+ */
227
+ openDirection: {
228
+ type: String,
229
+ default: 'auto',
230
+ },
231
+
232
+ taggable: {
233
+ type: Boolean,
234
+ default: false,
235
+ },
236
+ /**
237
+ * Помечать в списке выбранные
238
+ * элементы
239
+ */
240
+ showLabels: {
241
+ type: Boolean,
242
+ default: false,
243
+ },
244
+
245
+ internalSearch: {
246
+ type: Boolean,
247
+ default: true,
248
+ },
249
+ /**
250
+ * Значение
251
+ */
252
+ // eslint-disable-next-line vue/require-prop-types
253
+ value: {
254
+ default: null,
255
+ },
256
+ /**
257
+ * Ошибки
258
+ */
259
+ errors: {
260
+ type: Array,
261
+ default: null,
262
+ },
263
+ /**
264
+ * Name
265
+ */
266
+ name: {
267
+ type: String,
268
+ required: true,
269
+ },
270
+ /**
271
+ * Если нужен тултип
272
+ * над элементами списка
273
+ */
274
+ optionsTooltip: {
275
+ type: Boolean,
276
+ default: false,
277
+ },
278
+ /**
279
+ * Если режим taggable && searchValueInOptions то добавлять введенный тег в опции
280
+ */
281
+ searchValueInOptions: {
282
+ type: Boolean,
283
+ default: true,
284
+ },
285
+ /**
286
+ * Группировка { label: 'label text', values: Array of objects }
287
+ */
288
+ groupKeys: {
289
+ type: Object,
290
+ default: null,
291
+ },
292
+ required: {
293
+ type: Boolean,
294
+ default: false,
295
+ },
296
+ /**
297
+ * Если айтемам в селекте нужны превью с иконками и описанием
298
+ */
299
+ optionWithPreview: {
300
+ type: Boolean,
301
+ default: false,
302
+ },
303
+ tabindex: {
304
+ type: [String, Number],
305
+ default: null,
306
+ },
307
+ /**
308
+ * Если нужно ограничить максимальную высоту блока с выбранными элементами
309
+ */
310
+ maxHeight: {
311
+ type: String,
312
+ default: null,
313
+ },
314
+ /**
315
+ * Рендерить ли выпадающий список абсолютно, что бы помещался в ограниченном пространстве
316
+ * */
317
+ renderAbsoluteList: {
318
+ type: Boolean,
319
+ default: false,
320
+ },
321
+ /**
322
+ * Для какого языка селект
323
+ */
324
+ locale: {
325
+ type: String,
326
+ default: null,
327
+ },
328
+ /**
329
+ * Текст для пустого селекта, когда неичего не найдено
330
+ */
331
+ noResultsText: {
332
+ type: String,
333
+ default: 'No results',
334
+ },
335
+ /**
336
+ * Показывать ли состояние лоадинга
337
+ */
338
+ loading: {
339
+ type: Boolean,
340
+ default: false,
341
+ },
342
+ /**
343
+ * Ограничить ли отображение выбранных опций
344
+ */
345
+ collapsed: {
346
+ type: Boolean,
347
+ default: false,
348
+ },
349
+ },
350
+ data() {
351
+ return {
352
+ searchValue: null,
353
+ key: `field_select_${Date.now()}`,
354
+ field_key: `field-${this.name}`,
355
+ closest_scroll_element: null,
356
+ scroll_resize_observer: null,
357
+ local_options: [],
358
+ custom_limit: 0,
359
+ is_show_all_options: false,
360
+ }
361
+ },
362
+ computed: {
363
+ tagBind() {
364
+ return {
365
+ label: 'name',
366
+ trackBy: 'value',
367
+ value: this._value,
368
+ loading: this.loading,
369
+ options: this.collapsed ? this.visibleOptions : this.computedOptions,
370
+ searchable: this.searchable,
371
+ showLabels: this.showLabels,
372
+ multiple: this.multiple,
373
+ hideSelected: this.hideSelected,
374
+ allowEmpty: this.allowEmpty,
375
+ openDirection: this.openDirection,
376
+ id: this.name,
377
+ taggable: this.taggable,
378
+ tagPlaceholder: '',
379
+ placeholder: this.placeholder,
380
+ disabled: this.disabled,
381
+ internalSearch: this.internalSearch,
382
+ tabindex: +this.tabindex,
383
+ ...(this.groupKeys ? { groupLabel: this.groupKeys.label } : {}),
384
+ ...(this.groupKeys ? { groupValues: this.groupKeys.values } : {}),
385
+ ...(this.collapsed ? { limit: this.is_show_all_options ? this.value?.length : this.custom_limit } : {}),
386
+ class: this.collapsed ? 'mc-field-select__limit' : '',
387
+ }
388
+ },
389
+ visibleOptions() {
390
+ return this.is_show_all_options ? this.computedOptions : this.computedOptions.slice(0, this.custom_limit)
391
+ },
392
+ computedCaretClass() {
393
+ return {
394
+ multiselect__select: true,
395
+ 'mc-field-select__limit-toggle': true,
396
+ 'mc-field-select__limit-toggle--close': this.is_show_all_options,
397
+ }
398
+ },
399
+ limitText() {
400
+ return `+${+this.value?.length - +this.custom_limit}`
401
+ },
402
+ isShowLimitToggle() {
403
+ return this.collapsed && this.value?.length > this.custom_limit
404
+ },
405
+ hasTitle() {
406
+ return !!this.title
407
+ },
408
+ /**
409
+ * Если режим taggable && searchValueInOptions то добавлять введенный тег в опции
410
+ * **/
411
+ computedOptions() {
412
+ let options = !this.groupKeys
413
+ ? [...this.options, ...this.local_options].filter(
414
+ (v, i, a) => a.findIndex(afi => afi.value === v.value) === i,
415
+ )
416
+ : this.options
417
+ if (this.searchValueInOptions && this.taggable) {
418
+ const search = this.searchValue
419
+ return search && search.length ? [{ name: search, value: search }, ...options] : options
420
+ }
421
+ return options
422
+ },
423
+ rtl() {
424
+ return LANGUAGES.rtl.includes(this.locale)
425
+ },
426
+ dir() {
427
+ return this.rtl ? 'rtl' : null
428
+ },
429
+ classes() {
430
+ return {
431
+ 'mc-field-select': true,
432
+ 'mc-field-select--error': this.errorText,
433
+ 'mc-field-select--disabled': this.disabled,
434
+ [`mc-field-select--bg-${this.backgroundColor}`]: this.backgroundColor,
435
+ 'mc-field-select--is-empty-options-list': this.isEmptyOptions,
436
+ 'mc-field-select--with-preview': this.optionWithPreview,
437
+ 'mc-field-select--max-height': this.maxHeight,
438
+ 'mc-field-select--rtl': this.rtl,
439
+ }
440
+ },
441
+ isEmptyOptions() {
442
+ return this.isEmptyOptionsList || this.loading || this.computedOptions?.length === this._value?.length
443
+ },
444
+ computedTitle() {
445
+ return `${this.title}${this.required ? ' *' : ''}`
446
+ },
447
+ styles() {
448
+ const darkColors = ['gray', 'dark-gray', 'black']
449
+ const lightColors = ['white']
450
+ let placeHolderColor
451
+ let borderColor = this.backgroundColor
452
+ let backgroundColor = this.backgroundColor
453
+ let labelColor
454
+ if (!this.backgroundColor || lightColors.includes(this.backgroundColor)) {
455
+ borderColor = 'purple'
456
+ }
457
+ if (darkColors.includes(this.backgroundColor)) {
458
+ labelColor = 'white'
459
+ placeHolderColor = 'white'
460
+ borderColor = 'black'
461
+ }
462
+ if (this.disabled && !this.backgroundColor) {
463
+ backgroundColor = 'hover-gray'
464
+ }
465
+ return {
466
+ '--mc-field-select-max-height': this.maxHeight,
467
+ '--mc-field-select-color': backgroundColor && `var(--color-${backgroundColor})`,
468
+ '--mc-field-select-border-color': borderColor && `var(--color-${borderColor})`,
469
+ '--mc-field-select-label-color': labelColor && `var(--color-${labelColor})`,
470
+ '--mc-field-select-placeholder-color': placeHolderColor && `var(--color-${placeHolderColor})`,
471
+ }
472
+ },
473
+ _value() {
474
+ if (this.multiple) {
475
+ if (this.value === null) return []
476
+ let result = []
477
+ for (let value of this.value) {
478
+ const options = [
479
+ ...(this.groupKeys
480
+ ? this.options.map(o => o[this.groupKeys.values]).flat()
481
+ : this.computedOptions),
482
+ ]
483
+ let option = options.find(o => {
484
+ if (o.value?.hasOwnProperty('id') && o.value.id == value.id) {
485
+ return true
486
+ }
487
+ return o.value == value
488
+ })
489
+ if (option !== null) result.push(option)
490
+ }
491
+ return result
492
+ }
493
+ if (this.groupKeys) {
494
+ let ungruppedOptions = []
495
+ for (let option of this.options) {
496
+ ungruppedOptions.push(...option[this.groupKeys.values])
497
+ }
498
+ return ungruppedOptions.find(o => o.value == this.value)
499
+ }
500
+ return this.computedOptions.find(o => o.value == this.value)
501
+ },
502
+
503
+ isEmptyOptionsList() {
504
+ if ((this.hideSelected && !this.searchValue) || !this.options.length) {
505
+ if (this.multiple) {
506
+ if (this.groupKeys) return false
507
+ return this.options.length === this._value.length
508
+ } else {
509
+ return this._value && this.computedOptions.length === 1 && !this.searchValue
510
+ }
511
+ } else if (this.options.length === 0) return !this.options.length
512
+ return false
513
+ },
514
+ hasPrepend() {
515
+ return this.avatar || this.icon
516
+ },
517
+ },
518
+ watch: {
519
+ options: {
520
+ immediate: true,
521
+ handler(val) {
522
+ //Пушим все входящие опции в локальные опции
523
+ this.local_options.push(...val)
524
+ this.actualizeSavedOptions()
525
+ this.calcLimit()
526
+ },
527
+ },
528
+ value: {
529
+ deep: true,
530
+ immediate: true,
531
+ handler() {
532
+ this.actualizeSavedOptions()
533
+ this.calcLimit()
534
+ },
535
+ },
536
+ },
537
+ methods: {
538
+ actualizeSavedOptions() {
539
+ //Фильтруем локальные опции и оставляем только те, значения которых выбраны в селекте
540
+ this.local_options = this.local_options.filter(lo =>
541
+ this.value?.constructor === Array
542
+ ? this.value.map(v => String(v)).includes(String(lo.value))
543
+ : String(lo.value) === String(this.value),
544
+ )
545
+
546
+ //Делаем Юник, что бы опции не повторялись
547
+ this.local_options = this.local_options.filter(
548
+ (v, i, a) => a.findIndex(afi => String(afi.value) === String(v.value)) === i,
549
+ )
550
+ },
551
+ handleOpen() {
552
+ if (!this.renderAbsoluteList) return
553
+ this.initScroll()
554
+ },
555
+ handleClose() {
556
+ this.closest_scroll_element?.removeEventListener('scroll', this.repositionDropDown)
557
+ this.scroll_resize_observer?.disconnect()
558
+ this.scroll_resize_observer = null
559
+ },
560
+ findClosestScrollElement(element) {
561
+ if (!element) return document.documentElement
562
+ const { overflow, overflowY } = getComputedStyle(element)
563
+ const scrollableVariants = ['auto', 'scroll']
564
+ return scrollableVariants.some(v => [overflow, overflowY].includes(v))
565
+ ? element
566
+ : this.findClosestScrollElement(element.parentNode)
567
+ },
568
+ initScroll() {
569
+ // looking for closest scroll elemen to track select list position dynamically
570
+ this.closest_scroll_element = this.findClosestScrollElement(this.$refs[this.field_key])
571
+ this.closest_scroll_element.addEventListener('scroll', this.repositionDropDown)
572
+ this.scroll_resize_observer = new ResizeObserver(this.repositionDropDown)
573
+ this.scroll_resize_observer.observe(this.closest_scroll_element)
574
+ },
575
+ repositionDropDown() {
576
+ const { top, bottom, height, width, left } = this.$el.getBoundingClientRect()
577
+ const ref = this.$refs[this.key]
578
+ if (!ref) return
579
+ const ios_devices = ['iPhone', 'iPad']
580
+ // Добавляем к позиции отступ visualViewport?.offsetTop, который добавляет iOs при открытии вирутальной клавиатуры
581
+ const iosViewportIndent = ios_devices?.some(device => navigator?.platform?.includes(device))
582
+ ? window.visualViewport?.offsetTop || 0
583
+ : 0
584
+ // if field hides under scrolled element borders -> blur select to prevent overlap
585
+ if (top >= -height && bottom <= (window.innerHeight || document.documentElement.clientHeight)) {
586
+ ref.$refs.list.style.width = `${width}px`
587
+ ref.$refs.list.style.position = 'fixed'
588
+ ref.$refs.list.style.left = `${left}px`
589
+ const title_height = document.querySelector('.mc-field-select__header').offsetHeight
590
+ const title_margin = 8
591
+ let openDirection = this.openDirection
592
+ if (openDirection === 'auto') openDirection = ref?.isAbove ? 'top' : 'bottom'
593
+ switch (openDirection) {
594
+ case 'top':
595
+ ref.$refs.list.style.top = `${top +
596
+ (this.hasTitle ? title_height + title_margin : 0) +
597
+ iosViewportIndent -
598
+ ref.$refs.list.getBoundingClientRect().height -
599
+ 8}px`
600
+ ref.$refs.list.style.bottom = 'auto'
601
+ break
602
+ case 'bottom':
603
+ ref.$refs.list.style.bottom = 'auto'
604
+ ref.$refs.list.style.top = `${top + iosViewportIndent + height}px`
605
+ break
606
+ }
607
+ // Задержка для предотвращения закрытия выпадающего списка на android
608
+ const is_android = /Android/i.test(navigator.userAgent)
609
+ is_android && setTimeout(() => ref.activate(), 100) // переактивировать, если выпадающий список должен быть открыт
610
+ } else {
611
+ // прячем селект, если его не видно юзеру
612
+ return ref.deactivate()
613
+ }
614
+ },
615
+ handleChange(value) {
616
+ /**
617
+ * Истинное значение инпута
618
+ */
619
+ this.$emit('original-input', value)
620
+ if (value !== null) {
621
+ if (this.multiple) {
622
+ value = value.map(v => v.value)
623
+ } else {
624
+ value = value.value
625
+ }
626
+ }
627
+ this.emitInput(value)
628
+ },
629
+
630
+ handleTag(value) {
631
+ /**
632
+ * Событие по добавлению
633
+ * тега в инпут (по Enter)
634
+ * @property {string}
635
+ */
636
+ this.$emit('tag', value)
637
+ },
638
+
639
+ handleSearchChange(value) {
640
+ this.searchValue = value
641
+ /**
642
+ * Событие по вводу данных в инпут
643
+ * @property {string}
644
+ */
645
+ this.$emit('search-change', value)
646
+ this.renderAbsoluteList && this.$nextTick(() => this.repositionDropDown())
647
+ },
648
+
649
+ emitInput(value) {
650
+ this.toggleErrorVisible()
651
+ /**
652
+ * Событие инпута (выбранное значение)
653
+ * @property {array, number}
654
+ */
655
+ this.$emit('input', value)
656
+ },
657
+ /**
658
+ * Вычисляем custom_limit, которое ограничивает кол-во дочерних элементов внутри родительского, чтобы они не превышали его ширину
659
+ * */
660
+ calcLimit() {
661
+ if (!this.collapsed) return
662
+ this.$nextTick(() => {
663
+ this.custom_limit = Infinity
664
+ let child_width = 0
665
+ const parent = this.$refs[this.key]?.$refs?.tags?.firstChild
666
+ if (!this.value?.length) return
667
+ const limit_text_width = this.getLimitTextWidth() // Получаем ширину текста лимита
668
+ const total_width = +parent?.clientWidth - +limit_text_width
669
+ for (let i = 0; i < this.value?.length; i++) {
670
+ const children = parent?.children?.[i]
671
+ const elem_style = window.getComputedStyle(children)
672
+ child_width += children?.clientWidth + (parseInt(elem_style?.marginRight) || 0)
673
+ // считаем занимаемую дочерними элементами ширину, если превышает родительскую, то выходим из цикла и ставим лимит
674
+ if (+child_width > +total_width) {
675
+ this.custom_limit = i
676
+ break
677
+ }
678
+ }
679
+ })
680
+ },
681
+ getLimitTextWidth() {
682
+ const temp_limit_element = document.createElement('div')
683
+ temp_limit_element.style.visibility = 'hidden'
684
+ temp_limit_element.style.position = 'absolute'
685
+ temp_limit_element.innerText = `+${this.value?.length}` // Устанавливаем текст лимита
686
+ document.body.appendChild(temp_limit_element)
687
+ const limit_text_width = temp_limit_element.clientWidth
688
+ document.body.removeChild(temp_limit_element)
689
+ return limit_text_width
690
+ },
691
+ toggleOptions() {
692
+ this.is_show_all_options = !this.is_show_all_options
693
+ },
694
+ },
695
+ }
696
+ </script>
697
+
698
+ <style lang="scss">
699
+ @import 'vue-multiselect/dist/vue-multiselect.min';
700
+ @import '../../../styles/mixins';
701
+ @import '../../../tokens/durations';
702
+ @import '../../../tokens/font-families';
703
+ @import '../../../tokens/box-shadows';
704
+ @import '../../../tokens/colors';
705
+ @import '../../../tokens/font-sizes';
706
+ @import '../../../tokens/line-heights';
707
+ @import '../../../tokens/font-weights';
708
+ @import '../../../tokens/sizes';
709
+ @import '../../../tokens/spacings';
710
+ .mc-field-select {
711
+ $block-name: &;
712
+ --mc-field-select-color: initial;
713
+ --mc-field-select-label-color: #{$color-black};
714
+ --mc-field-select-border-color: initial;
715
+ --mc-field-select-max-height: initial;
716
+ --mc-field-select-placeholder-color: #{$color-gray};
717
+ @include custom-scroll($space-100);
718
+ font-family: $font-family-main;
719
+
720
+ &__header {
721
+ @include reset-text-indents();
722
+ display: block;
723
+ margin-bottom: $space-100;
724
+
725
+ &:empty {
726
+ display: none;
727
+ }
728
+ }
729
+
730
+ &__footer {
731
+ margin-top: $space-50;
732
+ line-height: $line-height-150;
733
+
734
+ &:empty {
735
+ display: none;
736
+ }
737
+ }
738
+
739
+ &__single-label {
740
+ @include reset-text-indents();
741
+ position: relative;
742
+ display: flex;
743
+ flex-wrap: nowrap;
744
+ align-items: center;
745
+ @include child-indent-right($space-50);
746
+ }
747
+
748
+ &__prepend {
749
+ position: absolute;
750
+ }
751
+
752
+ &__label-text {
753
+ @include ellipsis();
754
+ font-size: $font-size-200;
755
+ line-height: $line-height-200;
756
+ padding-inline-start: $space-50;
757
+ color: var(--mc-field-select-label-color);
758
+ &--indent-left {
759
+ margin-inline-start: $space-300;
760
+ }
761
+ }
762
+
763
+ .multiselect {
764
+ &__placeholder {
765
+ @include ellipsis();
766
+ color: var(--mc-field-select-placeholder-color);
767
+ font-size: $font-size-200;
768
+ line-height: $line-height-200;
769
+ margin-bottom: $space-150 - 1px;
770
+ padding-top: $space-150 - 1px;
771
+ padding-inline-start: $space-50;
772
+ width: 100%;
773
+ }
774
+
775
+ &__single {
776
+ padding-inline-start: 0;
777
+ margin-bottom: $space-150 - 1px;
778
+ margin-top: $space-150 - 1px;
779
+ background-color: transparent;
780
+ min-height: auto;
781
+
782
+ @include input-placeholder() {
783
+ color: $color-gray;
784
+ }
785
+ }
786
+
787
+ &__input {
788
+ padding-inline-start: $space-50;
789
+ margin-bottom: $space-150 - 2px;
790
+ padding-top: $space-150 - 1px;
791
+ font-size: $font-size-200;
792
+ line-height: $line-height-200;
793
+ min-height: auto;
794
+ background-color: $color-transparent;
795
+ @include input-placeholder() {
796
+ color: $color-gray;
797
+ }
798
+ }
799
+
800
+ &__select {
801
+ overflow: hidden;
802
+ height: $space-350;
803
+ width: $space-300;
804
+ inset-inline-end: $space-100;
805
+ top: 6px;
806
+ padding: 0;
807
+ z-index: 1;
808
+ &::before {
809
+ direction: ltr;
810
+ width: 0;
811
+ height: 0;
812
+ border-left: 5px solid transparent;
813
+ border-right: 5px solid transparent;
814
+ border-bottom: 5px solid transparent;
815
+ border-top: 5px solid var(--mc-field-select-label-color);
816
+ }
817
+ }
818
+
819
+ &__tags {
820
+ @include reset-text-indents();
821
+ position: relative;
822
+ border: 1px solid $color-outline-gray;
823
+ border-radius: $radius-100 !important;
824
+ padding: 0;
825
+ padding-inline: $space-100 $space-500;
826
+ overflow: hidden;
827
+ text-align: start;
828
+ &:hover {
829
+ border-color: $color-purple;
830
+ }
831
+ &:before {
832
+ content: '';
833
+ position: absolute;
834
+ left: 0;
835
+ top: 0;
836
+ @include size(100%);
837
+ background-color: var(--mc-field-select-color);
838
+ opacity: 0.6;
839
+ }
840
+ }
841
+
842
+ &__tags-wrap {
843
+ width: 100%;
844
+ position: relative;
845
+ padding-top: 4px;
846
+ padding-bottom: 3px;
847
+ top: 0;
848
+ display: flex;
849
+ flex-wrap: wrap;
850
+ margin-top: -1px;
851
+ min-height: $size-500 - 2px;
852
+ @include child-indent-right($space-100);
853
+ }
854
+
855
+ &__tag {
856
+ display: inline-flex;
857
+ align-items: center;
858
+ height: $size-300;
859
+ font-family: $font-family-main;
860
+ margin-top: $space-50;
861
+ margin-bottom: $space-50;
862
+ margin-right: unset;
863
+ background-color: $color-lighter-purple;
864
+ color: $color-black;
865
+ padding: $size-50 $size-50 $size-50 $size-100;
866
+ border-radius: 100px;
867
+ font-size: $font-size-200;
868
+ line-height: $line-height-200;
869
+
870
+ span {
871
+ @include ellipsis();
872
+ flex: 1 1 auto;
873
+ //overflow: visible;
874
+ }
875
+ }
876
+
877
+ &__tag-icon {
878
+ @include size($size-200);
879
+ position: relative;
880
+ background-color: $color-purple;
881
+ border-radius: $radius-circle;
882
+ flex: 0 0 auto;
883
+ margin-inline-start: $space-100;
884
+
885
+ &:hover {
886
+ background-color: $color-red;
887
+ }
888
+
889
+ &::after {
890
+ @include align(true, true, absolute);
891
+ top: 45%;
892
+ color: $color-white;
893
+ }
894
+ }
895
+
896
+ &__content {
897
+ padding: $size-100;
898
+ max-width: 100%;
899
+ font-size: $font-size-200;
900
+ line-height: $line-height-200;
901
+ }
902
+
903
+ &__content-wrapper {
904
+ top: calc(100% + #{$size-100});
905
+ border: none;
906
+ border-radius: $radius-150;
907
+ box-shadow: $shadow-s;
908
+ transition: opacity $duration-s ease;
909
+ overflow-y: auto;
910
+ overflow-x: hidden;
911
+ max-height: 300px;
912
+ }
913
+
914
+ &--above {
915
+ .multiselect__content-wrapper {
916
+ bottom: calc(100% + #{$size-100});
917
+ top: auto;
918
+ }
919
+ }
920
+
921
+ &__option {
922
+ min-height: $size-500;
923
+ display: flex;
924
+ align-items: center;
925
+ border-radius: $radius-100;
926
+ padding: $space-150;
927
+
928
+ span {
929
+ @include ellipsis();
930
+ }
931
+
932
+ &--highlight {
933
+ background-color: $color-hover-gray;
934
+ color: $color-black;
935
+ }
936
+ &--selected {
937
+ background-color: $color-lighter-purple !important;
938
+ color: $color-black !important;
939
+ font-weight: $font-weight-medium;
940
+ }
941
+
942
+ &--group.multiselect__option--disabled {
943
+ background-color: $color-white !important;
944
+ }
945
+ }
946
+
947
+ &--active {
948
+ .multiselect {
949
+ &__tags {
950
+ &:before {
951
+ background-color: $color-transparent;
952
+ }
953
+ border-color: var(--mc-field-select-border-color);
954
+ }
955
+ &__select {
956
+ &::before {
957
+ border-color: $color-purple $color-transparent $color-transparent;
958
+ }
959
+ }
960
+ }
961
+ }
962
+ &__spinner {
963
+ &:after,
964
+ &:before {
965
+ border-top-color: $color-purple;
966
+ @include size($space-300);
967
+ top: calc(50% - 5px);
968
+ left: calc(50% - 2px);
969
+ }
970
+ }
971
+ }
972
+
973
+ &--error {
974
+ .multiselect {
975
+ &__tags {
976
+ border-color: $color-red !important;
977
+ }
978
+ }
979
+ }
980
+
981
+ &--is-empty-options-list {
982
+ .multiselect {
983
+ &__content-wrapper {
984
+ display: none !important;
985
+ }
986
+ }
987
+ }
988
+
989
+ &--disabled {
990
+ cursor: not-allowed;
991
+ .multiselect--disabled {
992
+ opacity: 1;
993
+ background: transparent;
994
+ .multiselect {
995
+ &__placeholder {
996
+ color: $color-gray;
997
+ }
998
+ &__single {
999
+ & #{$block-name}__label-text {
1000
+ color: $color-gray;
1001
+ }
1002
+ }
1003
+ &__select {
1004
+ background-color: transparent;
1005
+ &::before {
1006
+ border-color: $color-outline-gray transparent transparent;
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ &--with-preview {
1014
+ .mc-preview {
1015
+ align-items: center;
1016
+ }
1017
+ .multiselect {
1018
+ &__content {
1019
+ padding: 0;
1020
+ }
1021
+ &__select {
1022
+ display: none;
1023
+ }
1024
+ &__single {
1025
+ margin: 0;
1026
+ }
1027
+ &__tags {
1028
+ padding: $space-200 0;
1029
+ padding-inline: $space-150;
1030
+ cursor: pointer;
1031
+ border-color: $color-outline-light;
1032
+ }
1033
+ &__option,
1034
+ &__content-wrapper,
1035
+ &__tags {
1036
+ border-radius: $radius-200 !important;
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ &--max-height {
1042
+ .multiselect {
1043
+ &__tags {
1044
+ max-height: var(--mc-field-select-max-height);
1045
+ overflow-y: auto;
1046
+ position: initial;
1047
+ }
1048
+ &__spinner {
1049
+ background: transparent;
1050
+ right: calc(#{$space-50} / 2);
1051
+ top: calc(#{$space-50} / 2);
1052
+ }
1053
+ }
1054
+ }
1055
+
1056
+ &__options-tooltip-target {
1057
+ overflow: hidden;
1058
+ text-overflow: ellipsis;
1059
+ white-space: nowrap;
1060
+ }
1061
+
1062
+ &--rtl {
1063
+ direction: rtl;
1064
+ }
1065
+
1066
+ &__limit {
1067
+ &-text {
1068
+ position: relative;
1069
+ z-index: 1;
1070
+ width: auto;
1071
+ }
1072
+ &-toggle {
1073
+ pointer-events: auto;
1074
+ &:before {
1075
+ border-color: black transparent transparent !important;
1076
+ }
1077
+ &--close {
1078
+ transform: rotate(180deg);
1079
+ }
1080
+ }
1081
+ .multiselect__tags {
1082
+ display: flex;
1083
+ justify-content: space-between;
1084
+ align-items: center;
1085
+ }
1086
+ }
1087
+ }
1088
+ </style>