@formixru/b2b-ui 3.0.2 → 3.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.
@@ -0,0 +1,185 @@
1
+ <template>
2
+ <div v-once ref="$root" class="range-input">
3
+ <div class="range-input__wrapper">
4
+ <input
5
+ ref="$from"
6
+ class="range-input__input input"
7
+ type="number"
8
+ :placeholder="`от ${initMin}`"
9
+ :min="initMin"
10
+ :value="initFromValue"
11
+ :step="initStep"
12
+ :name="fromName"
13
+ :disabled="initDisabled"
14
+ />
15
+ <input
16
+ ref="$to"
17
+ class="range-input__input input"
18
+ type="number"
19
+ :placeholder="`до ${initMax}`"
20
+ :max="initMax"
21
+ :value="initToValue"
22
+ :step="initStep"
23
+ :name="toName"
24
+ :disabled="initDisabled"
25
+ />
26
+ </div>
27
+ <div ref="$track" class="range-input__track"></div>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup>
32
+ import noUiSlider from 'nouislider';
33
+ import { onMounted, ref, toRefs } from 'vue';
34
+
35
+ const emit = defineEmits(['input']);
36
+
37
+ const props = defineProps({
38
+ fromName: {
39
+ type: String,
40
+ default: '',
41
+ },
42
+ toName: {
43
+ type: String,
44
+ default: '',
45
+ },
46
+ initFromValue: {
47
+ type: Number,
48
+ default: undefined,
49
+ },
50
+ initToValue: {
51
+ type: Number,
52
+ default: undefined,
53
+ },
54
+ initMin: {
55
+ type: Number,
56
+ required: true,
57
+ },
58
+ initMax: {
59
+ type: Number,
60
+ required: true,
61
+ },
62
+ initStep: {
63
+ type: Number,
64
+ default: 1,
65
+ },
66
+ initDisabled: {
67
+ type: Boolean,
68
+ },
69
+ throttle: {
70
+ type: Number,
71
+ default: 1000,
72
+ },
73
+ });
74
+
75
+ const { fromName, toName, initFromValue, initToValue, initMin, initMax, initStep, initDisabled, throttle } =
76
+ toRefs(props);
77
+
78
+ const $root = ref();
79
+ const $track = ref();
80
+ const $from = ref();
81
+ const $to = ref();
82
+
83
+ onMounted(() => {
84
+ const setTrackValue = () => {
85
+ const min = Number($from.value.min);
86
+ const max = Number($to.value.max);
87
+
88
+ let from;
89
+ let to;
90
+
91
+ // Иначе, если мин. и макс. одинаковые, ползунок перемещается в конец трека
92
+ if (min !== max) {
93
+ from = $from.value.value.length > 0 ? Number($from.value.value) : min;
94
+ to = $to.value.value.length > 0 ? Number($to.value.value) : max;
95
+ }
96
+
97
+ $track.value.noUiSlider.set([from, to]);
98
+ };
99
+
100
+ const setFromTo = () => {
101
+ const value = $track.value.noUiSlider.get();
102
+ if ($from.value.value.length > 0 || Number(value[0]) > Number($from.value.min)) {
103
+ $from.value.value = Number(value[0]);
104
+ }
105
+ if ($to.value.value.length > 0 || Number(value[1]) < Number($to.value.max)) {
106
+ $to.value.value = Number(value[1]);
107
+ }
108
+ };
109
+
110
+ noUiSlider.create($track.value, {
111
+ start: [
112
+ initFromValue.value === undefined ? initMin.value : initFromValue.value,
113
+ initToValue.value === undefined ? initMax.value : initToValue.value,
114
+ ],
115
+ step: initStep.value,
116
+ range: {
117
+ min: [initMin.value],
118
+ max: [initMax.value],
119
+ },
120
+ connect: true,
121
+ });
122
+
123
+ if (initDisabled.value) {
124
+ $track.value.noUiSlider.target.setAttribute('disabled', true);
125
+ }
126
+
127
+ $track.value.noUiSlider.on('slide', () => {
128
+ setFromTo();
129
+ });
130
+
131
+ $track.value.noUiSlider.on('change', () => {
132
+ emit('input');
133
+ });
134
+
135
+ let throttleTimeout;
136
+
137
+ for (const $el of [$from.value, $to.value]) {
138
+ // Обновляем параметры ползунка при изменении атрибутов полей
139
+ const observer = new MutationObserver((mutations) => {
140
+ for (const { attributeName } of mutations) {
141
+ if (attributeName === 'step') {
142
+ $track.value.noUiSlider.updateOptions({
143
+ step: Number($el.step),
144
+ });
145
+ } else if (attributeName === 'disabled') {
146
+ if ($el.disabled) {
147
+ $track.value.noUiSlider.target.setAttribute('disabled', true);
148
+ } else {
149
+ $track.value.noUiSlider.target.removeAttribute('disabled');
150
+ }
151
+ } else {
152
+ $track.value.noUiSlider.updateOptions({
153
+ range: {
154
+ min: [Number($from.value.min)],
155
+ max: [Number($to.value.max)],
156
+ },
157
+ });
158
+ setTrackValue();
159
+ // Обновляем подсказки от/до
160
+ $from.value.placeholder = `от ${$from.value.min}`;
161
+ $to.value.placeholder = `до ${$to.value.max}`;
162
+ }
163
+ }
164
+ });
165
+ observer.observe($el, {
166
+ attributes: true,
167
+ attributeFilter: ['min', 'max', 'step', 'disabled'],
168
+ });
169
+
170
+ $el.addEventListener('input', () => {
171
+ setTrackValue();
172
+ const $filterItem = $root.value.closest('.filter__item');
173
+ $filterItem._tooltip?.hide();
174
+ clearTimeout(throttleTimeout);
175
+ throttleTimeout = setTimeout(() => {
176
+ emit('input');
177
+ }, throttle.value);
178
+ });
179
+
180
+ $el.addEventListener('change', () => {
181
+ setFromTo();
182
+ });
183
+ }
184
+ });
185
+ </script>
@@ -0,0 +1,328 @@
1
+ <template>
2
+ <div ref="$root" class="suggest-input" @keydown.esc="esc()">
3
+ <input
4
+ :id="id"
5
+ ref="$input"
6
+ v-model="model"
7
+ class="suggest-input__input input"
8
+ :class="[
9
+ showSuggests ? 'suggest-input__input_active' : '',
10
+ small ? 'input_small' : '',
11
+ error ? 'input_error' : '',
12
+ ...inputCssClasses,
13
+ ]"
14
+ type="text"
15
+ :name="name"
16
+ :required="required"
17
+ :disabled="disabled"
18
+ :placeholder="placeholder"
19
+ autocomplete="off"
20
+ @input="handleInput()"
21
+ @focusin="focusin()"
22
+ @focusout="focusout($event)"
23
+ @keydown="arrowNavigation($event)"
24
+ />
25
+ <div v-show="isLoading" class="suggest-input__loader"><div class="spinner"></div></div>
26
+ <button v-show="isClearVisible" ref="$clear" class="suggest-input__clear" type="button" @click="clear()">
27
+ <svg class="suggest-input__clear-icon"><use :href="`${ICONS_PATH}#close`" /></svg>
28
+ </button>
29
+ <div v-show="showSuggests" ref="$list" class="suggest-input__list" @mouseover="focusSuggestIndex = -1">
30
+ <template v-if="suggests.length > 0">
31
+ <div
32
+ v-for="(item, index) in formattedSuggests"
33
+ :key="index"
34
+ class="suggest-input__list-item"
35
+ :class="{ 'suggest-input__list-item_focus': index === focusSuggestIndex }"
36
+ tabindex="0"
37
+ @click="select(item)"
38
+ >
39
+ <span v-if="hightlightMatches" v-html="item.formatted_label"></span>
40
+ <span v-else v-html="item.label"></span>
41
+ </div>
42
+ </template>
43
+ <div v-else class="suggest-input__not-found" v-html="notFoundMessage"></div>
44
+ </div>
45
+ <slot name="content"></slot>
46
+ </div>
47
+ </template>
48
+
49
+ <script setup>
50
+ /* globals BX */
51
+
52
+ import { computed, nextTick, onMounted, ref, toRefs, watch } from 'vue';
53
+
54
+ import { errorHandler } from '../../utils/error-handler.js';
55
+
56
+ const ICONS_PATH = window.ICONS_PATH;
57
+
58
+ const emit = defineEmits(['focusin', 'focusout', 'select']);
59
+
60
+ const model = defineModel({ type: String, default: '' });
61
+
62
+ const props = defineProps({
63
+ id: {
64
+ type: String,
65
+ default: '',
66
+ },
67
+ name: {
68
+ type: String,
69
+ default: '',
70
+ },
71
+ placeholder: {
72
+ type: String,
73
+ default: ' ',
74
+ },
75
+ required: {
76
+ type: Boolean,
77
+ },
78
+ disabled: {
79
+ type: Boolean,
80
+ },
81
+ action: {
82
+ type: String,
83
+ required: true,
84
+ },
85
+ queryName: {
86
+ type: String,
87
+ default: 'query',
88
+ },
89
+ minlength: {
90
+ type: Number,
91
+ default: 3,
92
+ },
93
+ throttle: {
94
+ type: Number,
95
+ default: 1000,
96
+ },
97
+ inputCssClasses: {
98
+ type: Array,
99
+ default() {
100
+ return [];
101
+ },
102
+ },
103
+ hightlightMatches: {
104
+ type: Boolean,
105
+ },
106
+ notFoundMessage: {
107
+ type: String,
108
+ default: 'По&nbsp;вашему запросу ничего не&nbsp;найдено',
109
+ },
110
+ small: {
111
+ type: Boolean,
112
+ },
113
+ error: {
114
+ type: Boolean,
115
+ },
116
+ });
117
+
118
+ const {
119
+ id,
120
+ name,
121
+ placeholder,
122
+ required,
123
+ disabled,
124
+ action,
125
+ queryName,
126
+ minlength,
127
+ throttle,
128
+ inputCssClasses,
129
+ hightlightMatches,
130
+ small,
131
+ notFoundMessage,
132
+ } = toRefs(props);
133
+
134
+ const $root = ref();
135
+ const $input = ref();
136
+ const $list = ref();
137
+ const $clear = ref();
138
+
139
+ const suggests = ref([]);
140
+ const formattedSuggests = ref([]);
141
+ const focusSuggestIndex = ref(-1);
142
+
143
+ watch(suggests, () => {
144
+ focusSuggestIndex.value = -1;
145
+ const regexp = new RegExp(model.value, 'i');
146
+ formattedSuggests.value = suggests.value.map((item) => {
147
+ if (hightlightMatches.value) {
148
+ const match = regexp.exec(item.label);
149
+ item.formatted_label = match ? item.label.replace(match[0], `<mark>${match[0]}</mark>`) : item.label;
150
+ }
151
+ return item;
152
+ });
153
+ });
154
+
155
+ const showSuggests = ref(false);
156
+ const itemHeight = ref(0);
157
+
158
+ watch(showSuggests, async (value) => {
159
+ if (value && suggests.value.length > 0) {
160
+ await nextTick();
161
+ itemHeight.value = $list.value.querySelector('.suggest-input__list-item').offsetHeight;
162
+ } else {
163
+ suggests.value = [];
164
+ await nextTick();
165
+ itemHeight.value = 0;
166
+ }
167
+ });
168
+
169
+ const maxItemsWithoutScroll = computed(() => {
170
+ if (itemHeight.value) {
171
+ return Math.floor($list.value.offsetHeight / itemHeight.value);
172
+ }
173
+ return 0;
174
+ });
175
+
176
+ const indexesToScroll = computed(() => {
177
+ if (maxItemsWithoutScroll.value) {
178
+ const indexes = [0];
179
+ for (const index of suggests.value.keys()) {
180
+ const nextIndex = indexes.at(-1) + maxItemsWithoutScroll.value;
181
+ if (nextIndex > suggests.value.length) {
182
+ break;
183
+ }
184
+ if (index === nextIndex) {
185
+ indexes.push(index);
186
+ }
187
+ }
188
+ return indexes;
189
+ }
190
+ return [];
191
+ });
192
+
193
+ const isLoading = ref(false);
194
+ const isClearVisible = computed(() => model.value.length >= minlength.value && !isLoading.value);
195
+
196
+ let throttleTimeout;
197
+
198
+ const handleInput = () => {
199
+ clearTimeout(throttleTimeout);
200
+ if (model.value.length >= minlength.value) {
201
+ throttleTimeout = setTimeout(async () => {
202
+ isLoading.value = true;
203
+ const url = new URL(action.value, location.origin);
204
+ url.searchParams.append(queryName.value, model.value);
205
+ try {
206
+ const response = await fetch(url, {
207
+ headers: {
208
+ 'X-Requested-With': 'XMLHttpRequest',
209
+ 'X-Session-Id': BX.message('bitrix_sessid'),
210
+ },
211
+ });
212
+ if (!response.ok) {
213
+ throw response;
214
+ }
215
+ const data = await response.json();
216
+ isLoading.value = false;
217
+ suggests.value = data;
218
+ showSuggests.value = true;
219
+ } catch (error) {
220
+ if (error.status === 401) {
221
+ location.reload();
222
+ } else {
223
+ isLoading.value = false;
224
+ errorHandler(error);
225
+ }
226
+ }
227
+ }, throttle.value);
228
+ } else {
229
+ showSuggests.value = false;
230
+ }
231
+ };
232
+
233
+ const focusin = async () => {
234
+ emit('focusin');
235
+ await nextTick();
236
+ handleInput();
237
+ };
238
+
239
+ const focusout = async ({ relatedTarget }) => {
240
+ if (!relatedTarget || (!$list.value.contains(relatedTarget) && !$clear.value.contains(relatedTarget))) {
241
+ showSuggests.value = false;
242
+ }
243
+ await nextTick();
244
+ emit('focusout');
245
+ };
246
+
247
+ const esc = () => {
248
+ // Ждём, чтобы событие нажатия Esc сработало у других обработчиков (например, в диалоговых окнах)
249
+ setTimeout(() => {
250
+ showSuggests.value = false;
251
+ });
252
+ };
253
+
254
+ const clear = () => {
255
+ clearTimeout(throttleTimeout);
256
+ model.value = '';
257
+ showSuggests.value = false;
258
+ };
259
+
260
+ const select = ({ label, value }) => {
261
+ model.value = label;
262
+ showSuggests.value = false;
263
+ emit('select', {
264
+ input: model.value,
265
+ value,
266
+ });
267
+ };
268
+
269
+ const arrowNavigation = (event) => {
270
+ if (showSuggests.value) {
271
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
272
+ const selectionStart = $input.value.selectionStart;
273
+ const selectionEnd = $input.value.selectionEnd;
274
+ setTimeout(() => {
275
+ $input.value.setSelectionRange(selectionStart, selectionEnd);
276
+ });
277
+ switch (event.key) {
278
+ case 'ArrowDown': {
279
+ focusSuggestIndex.value =
280
+ suggests.value.length === focusSuggestIndex.value + 1 ? 0 : focusSuggestIndex.value + 1;
281
+ if (
282
+ focusSuggestIndex.value !== 0 &&
283
+ focusSuggestIndex.value !== suggests.value.length - 1 &&
284
+ indexesToScroll.value.includes(focusSuggestIndex.value)
285
+ ) {
286
+ $list.value.scrollTop += maxItemsWithoutScroll.value * itemHeight.value;
287
+ } else if (focusSuggestIndex.value === 0) {
288
+ $list.value.scrollTop = 0;
289
+ }
290
+ break;
291
+ }
292
+ case 'ArrowUp': {
293
+ focusSuggestIndex.value =
294
+ focusSuggestIndex.value <= 0 ? suggests.value.length - 1 : focusSuggestIndex.value - 1;
295
+ if (
296
+ focusSuggestIndex.value !== 0 &&
297
+ focusSuggestIndex.value !== suggests.value.length - 1 &&
298
+ indexesToScroll.value.includes(focusSuggestIndex.value + 1)
299
+ ) {
300
+ $list.value.scrollTop -= maxItemsWithoutScroll.value * itemHeight.value;
301
+ } else if (focusSuggestIndex.value === suggests.value.length - 1) {
302
+ $list.value.scrollTop = suggests.value.length * itemHeight.value;
303
+ }
304
+ break;
305
+ }
306
+ }
307
+ }
308
+ if (event.key === 'Enter' && focusSuggestIndex.value >= 0) {
309
+ // Ждём, чтобы событие нажатия Enter сработало у других обработчиков (например, в формах)
310
+ setTimeout(() => {
311
+ select(suggests.value[focusSuggestIndex.value]);
312
+ });
313
+ }
314
+ }
315
+ };
316
+
317
+ onMounted(() => {
318
+ $root.value.closest('.form')?.addEventListener('keydown', (event) => {
319
+ if (event.key === 'Enter' && focusSuggestIndex.value >= 0) {
320
+ event.preventDefault();
321
+ }
322
+ });
323
+ });
324
+
325
+ defineExpose({
326
+ clear,
327
+ });
328
+ </script>
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <div class="warning-message" :class="classes">
3
+ <svg v-if="!noIcon" class="warning-message__icon"><use :href="`${ICONS_PATH}#${icon}`" /></svg>
4
+ <div class="warning-message__wrapper">
5
+ <div class="warning-message__content">
6
+ <div v-if="title" class="warning-message__title" v-html="title"></div>
7
+ <div v-if="descr" class="warning-message__descr" v-html="descr"></div>
8
+ </div>
9
+ <a
10
+ v-if="actionUrl.length > 0"
11
+ class="warning-message__action button"
12
+ :class="{
13
+ button_inverted: solid && type !== 'light-info',
14
+ button_alt: !solid || type === 'light-info',
15
+ }"
16
+ :href="actionUrl"
17
+ >
18
+ <svg v-if="actionIcon.length > 0" class="button__icon">
19
+ <use :href="`${ICONS_PATH}#${actionIcon}`" />
20
+ </svg>
21
+ Ссылка
22
+ </a>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { computed, toRefs } from 'vue';
29
+
30
+ const ICONS_PATH = window.ICONS_PATH;
31
+
32
+ const props = defineProps({
33
+ type: {
34
+ type: String,
35
+ default: 'warning',
36
+ validator(value) {
37
+ return ['warning', 'success', 'info', 'light-info', 'error'].includes(value);
38
+ },
39
+ },
40
+ small: {
41
+ type: Boolean,
42
+ },
43
+ solid: {
44
+ type: Boolean,
45
+ },
46
+ title: {
47
+ type: String,
48
+ default: '',
49
+ },
50
+ descr: {
51
+ type: String,
52
+ default: '',
53
+ },
54
+ actionUrl: {
55
+ type: String,
56
+ default: '',
57
+ },
58
+ actionIcon: {
59
+ type: String,
60
+ default: '',
61
+ },
62
+ noIcon: {
63
+ type: Boolean,
64
+ },
65
+ });
66
+
67
+ const { type, small, solid, title, descr, actionUrl, actionIcon, noIcon } = toRefs(props);
68
+
69
+ const classes = computed(() => {
70
+ return {
71
+ 'warning-message_small': small.value,
72
+ 'warning-message_solid': solid.value,
73
+ 'warning-message_success': type.value === 'success',
74
+ 'warning-message_info': type.value === 'info',
75
+ 'warning-message_light-info': type.value === 'light-info',
76
+ 'warning-message_error': type.value === 'error',
77
+ };
78
+ });
79
+
80
+ const icon = computed(() => {
81
+ let icon;
82
+ switch (type.value) {
83
+ case 'success': {
84
+ icon = 'check';
85
+ break;
86
+ }
87
+ case 'info': {
88
+ icon = 'info';
89
+ break;
90
+ }
91
+ case 'error': {
92
+ icon = 'delete';
93
+ break;
94
+ }
95
+ default: {
96
+ icon = 'warning';
97
+ }
98
+ }
99
+ return icon;
100
+ });
101
+ </script>
@@ -978,7 +978,7 @@ $tabs-label-font: ('font-weight': bold, 'font-size': $font-size, 'line-height':
978
978
 
979
979
  $tabs-counter-bg: $color-grey-80 !default;
980
980
  $tabs-counter-color: $font-color-primary !default;
981
- $tabs-counter-font: ('font-size': $font-size-xs) !default;
981
+ $tabs-counter-font: ('font-size': 9px) !default;
982
982
  $tabs-counter-radius: $radius-s !default;
983
983
 
984
984
  /********** Всплывающие подсказки / Tooltips **********/