@studio-west/component-sw 0.11.9 → 0.11.11

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 (49) hide show
  1. package/README.md +5 -5
  2. package/dist/SwButton-yS_tKW9w.js +4 -0
  3. package/dist/{SwButton-jKDKwFV9.js → SwButton.vue_vue_type_script_setup_true_lang-aODPwFa6.js} +11 -11
  4. package/dist/{SwDatePicker-CJjKSM8k.js → SwDatePicker-CpmdOhuc.js} +1 -1
  5. package/dist/{SwDropdownItem-CtlMVgsX.js → SwDropdownItem-BE6ZRWT1.js} +1 -1
  6. package/dist/{SwGide-DbSSyZ-y.js → SwGide-_a5-3g_f.js} +2 -2
  7. package/dist/SwInput-CbNd7Vin.js +90 -0
  8. package/dist/{SwMessage-CovKkpf6.js → SwMessage-DdUbYQet.js} +6 -6
  9. package/dist/SwSection-CQe2kE0O.js +34 -0
  10. package/dist/SwSelect-BxbCfof-.js +1883 -0
  11. package/dist/{SwSlider-YncjYKPw.js → SwSlider-jWTzzPZg.js} +1 -1
  12. package/dist/SwSwitch-DeMdyD0-.js +47 -0
  13. package/dist/index-C3iiqwEz.js +188 -0
  14. package/dist/index.cjs +6 -1
  15. package/dist/index.js +1 -1
  16. package/package.json +6 -2
  17. package/src/Alert.ts +65 -0
  18. package/src/components/SwAlert.vue +70 -0
  19. package/src/components/SwButton.vue +50 -0
  20. package/src/components/SwButtonGroup.vue +67 -0
  21. package/src/components/SwCollapse.vue +36 -0
  22. package/src/components/SwDatePicker.vue +375 -0
  23. package/src/components/SwDropdown.vue +202 -0
  24. package/src/components/SwDropdownItem.vue +26 -0
  25. package/src/components/SwDropdownNew.vue +175 -0
  26. package/src/components/SwFormItem.vue +21 -0
  27. package/src/components/SwGide.vue +128 -0
  28. package/src/components/SwIcon.vue +16 -0
  29. package/src/components/SwInput.vue +100 -0
  30. package/src/components/SwMessage.vue +53 -0
  31. package/src/components/SwSection.vue +17 -0
  32. package/src/components/SwSelect.vue +151 -0
  33. package/src/components/SwSkeleton.vue +13 -0
  34. package/src/components/SwSkeletonItem.vue +27 -0
  35. package/src/components/SwSlider.vue +281 -0
  36. package/src/components/SwSwitch.vue +51 -0
  37. package/src/components/SwTable.vue +239 -0
  38. package/src/components/SwTableColumn.vue +25 -0
  39. package/src/components/SwTabs.vue +41 -0
  40. package/src/components/SwTabsPane.vue +44 -0
  41. package/src/index.ts +43 -0
  42. package/src/utils/index.ts +149 -0
  43. package/types/components.d.ts +72 -0
  44. package/types/index.d.ts +82 -70
  45. package/dist/SwInput-DCV1rrWa.js +0 -89
  46. package/dist/SwSection-D8ooQ21I.js +0 -37
  47. package/dist/SwSelect-C2RKinez.js +0 -72
  48. package/dist/SwSwitch-6rl1IT4p.js +0 -47
  49. package/dist/index-B5koqczP.js +0 -190
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <li :class="style">
3
+ <sw-icon :icon-class="iconBefore" v-if="iconBefore.length > 0"/>
4
+ <slot></slot>
5
+ </li>
6
+ </template>
7
+
8
+ <script setup>
9
+ import { computed } from "vue"
10
+ import SwIcon from "@/components/SwIcon.vue"
11
+
12
+ const props = defineProps({
13
+ class: {type: String, default: ''},
14
+ size: {type: String, default: ''},
15
+ type: {type: String, default: ''},
16
+ iconBefore: {type: String, default: ''},
17
+ })
18
+ const style = computed(() =>{
19
+ let s = ['sw-dropdown-item']
20
+ if(props.size.length > 0) s.push('sw-' + props.size)
21
+ if(props.type.length > 0) s.push('sw-' + props.type)
22
+ if(props.class.length > 0) s.push(props.class)
23
+ return s
24
+ }
25
+ )
26
+ </script>
@@ -0,0 +1,175 @@
1
+ <template>
2
+ <div
3
+ :class="style"
4
+ @click="toggleClick()"
5
+ @mouseover.stop="toggleHover()"
6
+ @mouseleave.stop="toggleHover()"
7
+ @contextmenu.stop="toggleContext()"
8
+ ref="dropdownRef"
9
+ >
10
+ <slot></slot>
11
+ <Teleport to="body">
12
+ <ul
13
+ popover
14
+ ref="popupRef"
15
+ :id ="id"
16
+ class="sw-dropdown-popup"
17
+ :class="props.class"
18
+ v-if="manual || false"
19
+ :style="popupStyle"
20
+ tabindex="-1"
21
+ >
22
+ <slot name="dropdown"></slot>
23
+ </ul>
24
+ </Teleport>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup>
29
+ import { computed, ref, onMounted, onUnmounted, watchEffect } from "vue"
30
+
31
+ const props = defineProps({
32
+ class: {type: String, default: ''},
33
+ type: {type: String, default: ''},
34
+ trigger: {type: String, default: "click"},
35
+ placement: {type: String, default: 'bottom-left'},
36
+ maxWidth: {type: Number, default: 0}
37
+ })
38
+ const manual = defineModel()
39
+
40
+ const dropdownRef = ref(null)
41
+ const popupRef = ref(null)
42
+ const popupStyle = ref({})
43
+ const id = Math.ceil(Math.random() * 1000)
44
+ const repositionTrigger = ref(0)
45
+ const style = computed(() =>{
46
+ let s = ['sw-dropdown']
47
+ if(props.type.length > 0) s.push('sw-' + props.type)
48
+ return s
49
+ }
50
+ )
51
+ const resizeObserver = new ResizeObserver(() => {
52
+ if (manual.value) repositionTrigger.value++
53
+ if(repositionTrigger.value > 99 ) repositionTrigger.value = 1
54
+ })
55
+
56
+ watchEffect( () => {
57
+ // console.log('watchEffect', repositionTrigger.value)
58
+ // Если popup не активен — ничего не делаем
59
+ if (!manual.value || !popupRef.value || !dropdownRef.value) {
60
+ return
61
+ }
62
+ if(repositionTrigger.value < 2) resizeObserver.observe(popupRef.value)
63
+ let scrollTop = window.pageYOffset || document.documentElement.scrollTop
64
+ let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
65
+ document.body.style.position = 'fixed'
66
+ document.body.style.top = `-${scrollTop}px`
67
+ document.body.style.left = `-${scrollLeft}px`
68
+ document.body.style.position = ''
69
+ document.body.style.top = ''
70
+ document.body.style.left = ''
71
+ document.documentElement.scrollTop = scrollTop
72
+ document.documentElement.scrollLeft = scrollLeft
73
+ const buttonRect = dropdownRef.value.getBoundingClientRect()
74
+ let viewportWidth = window.innerWidth
75
+ let viewportHeight = window.innerHeight
76
+ // Получаем высоту и ширину popup
77
+ let popupHeight = popupRef.value.offsetHeight;
78
+ let popupWidth = popupRef.value.offsetWidth;
79
+ // Расчет позиции popup (например, снизу кнопки)
80
+ // Центр по горизонтали
81
+ let left = buttonRect.left + (buttonRect.width / 2) + scrollLeft
82
+ // Центр по вертикали
83
+ let top = buttonRect.top + (buttonRect.height / 2) + scrollTop
84
+ let deltaTop = (buttonRect.height / 2) + 8
85
+ const [mainPlacement, subPlacement] = props.placement.split('-', 2)
86
+
87
+ // Основное позиционирование
88
+ switch (mainPlacement) {
89
+ case "bottom":
90
+ top = (scrollTop + viewportHeight - popupHeight - deltaTop - 8 < top)? top - deltaTop - popupHeight : top + deltaTop
91
+ if(subPlacement === undefined ) left -= popupWidth / 2
92
+ break
93
+ case "top":
94
+ top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
95
+ if(subPlacement === undefined ) left -= popupWidth / 2
96
+ break
97
+ case "left":
98
+ if(left - popupWidth - (buttonRect.width / 2) - 8 < 0) {
99
+ if(left + (buttonRect.width / 2) + 8 + popupWidth < viewportWidth) { // справа
100
+ left = left + (buttonRect.width / 2) + 8
101
+ top = top - (popupHeight / 2)
102
+ } else {
103
+ top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
104
+ left = (left + (buttonRect.width / 2) < viewportWidth )? left + (buttonRect.width / 2) - popupWidth: left - (popupWidth / 2)
105
+ }
106
+ } else { // слева
107
+ left = left - popupWidth - (buttonRect.width / 2) - 8
108
+ top = top - (popupHeight / 2)
109
+ }
110
+ break
111
+ case "right":
112
+ if(left + (buttonRect.width / 2) + 8 + popupWidth > viewportWidth ) {
113
+ if(left - popupWidth - (buttonRect.width / 2) - 8 > 0){
114
+ left = left - popupWidth - (buttonRect.width / 2) - 8 // слева
115
+ top = top - (popupHeight / 2)
116
+ } else {
117
+ top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
118
+ left = left - (buttonRect.width / 2) // влево
119
+ }
120
+ } else { // справа
121
+ left = left + (buttonRect.width / 2) + 8
122
+ top = top - (popupHeight / 2)
123
+ }
124
+ break
125
+ }
126
+ switch (subPlacement) {
127
+ case 'left':
128
+ left = (scrollLeft + viewportWidth - 8 > left - (buttonRect.width / 2) + popupWidth) ? left - (buttonRect.width / 2) : left - popupWidth + (buttonRect.width / 2)
129
+ break
130
+ case 'right':
131
+ left = (8< left + (buttonRect.width / 2) - popupWidth) ? left - popupWidth + (buttonRect.width / 2) : left - (buttonRect.width / 2)
132
+ break
133
+ }
134
+ // Убедимся, что попап не выходит за экран
135
+ if (left < 0) left = 0
136
+ if (top < 0) top = 0
137
+
138
+ popupStyle.value = {
139
+ position: 'absolute',
140
+ top: `${top}px`,
141
+ left: `${left}px`,
142
+ maxWidth: props.maxWidth > 0 ? `${props.maxWidth}px` : ''
143
+ }
144
+ }, {flush: 'post'}
145
+ )
146
+
147
+ const toggleClick = () => {
148
+ if(props.trigger === 'click') manual.value = !manual.value
149
+ }
150
+ const toggleHover = () => {
151
+ if(props.trigger === 'hover') manual.value = !manual.value
152
+ }
153
+ const toggleContext = () => {
154
+ if (props.trigger === 'context') manual.value = !manual.value
155
+ }
156
+ const handleResize = () => {
157
+ if (manual.value) repositionTrigger.value++
158
+ if(repositionTrigger.value > 99 ) repositionTrigger.value = 1
159
+ }
160
+ defineExpose({ handleResize })
161
+
162
+ onMounted(() => {
163
+ window.addEventListener("resize", handleResize)
164
+ if (window.onscrollend !== undefined) window.addEventListener("scrollend", handleResize)
165
+ else window.addEventListener("scroll", handleResize)
166
+ })
167
+
168
+ onUnmounted(() => {
169
+ resizeObserver.disconnect()
170
+ window.removeEventListener("resize", handleResize)
171
+ if (window.onscrollend !== undefined) window.removeEventListener("scrollend", handleResize)
172
+ else window.removeEventListener("scroll", handleResize)
173
+ })
174
+
175
+ </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <div :class="style">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ import {computed} from "vue"
9
+
10
+ const props = defineProps({
11
+ class: {type:String, default: ''},
12
+ type: {type:String, default: ''},
13
+ })
14
+ const style = computed(() =>{
15
+ let s = ['sw-form-item']
16
+ if(props.type.length > 0) s.push('sw-' + props.type)
17
+ if(props.class.length > 0) s.push(props.class)
18
+ return s
19
+ }
20
+ )
21
+ </script>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <div class="sw-gide" ref="swGide">
3
+ <div class="overlay0"/>
4
+ <div class="overlay1"/>
5
+ <sw-dropdown
6
+ class="gide"
7
+ trigger="none"
8
+ :placement="props.steps[step].placement"
9
+ v-model="visual"
10
+ :maxWidth='props.maxWidth'
11
+ >
12
+ <div class="hole"/>
13
+ <template #dropdown>
14
+ <header><slot name="header">{{ props.steps[step].header }} <sw-button type="primary" link @click="exitModule"><sw-icon :icon-class="props.iconClose" /></sw-button></slot></header>
15
+ <slot>
16
+ <div>{{ props.steps[step].text }}</div>
17
+ <img :class="(/left/.test(props.steps[step]?.placement))? 'reflect': null" :src="props.steps[step].img" />
18
+ </slot>
19
+ <footer>
20
+ <slot name="footer">
21
+ <sw-button :type="(step === 0)? 'info': 'warning'" text link @click="walk(-1)"><slot name="arrow">‹</slot></sw-button>
22
+ <sw-button type="success" @click="walk(1)" text link class="sw-revers"><slot name="arrow">‹</slot></sw-button>
23
+ </slot>
24
+ </footer>
25
+ </template>
26
+ </sw-dropdown>
27
+ <div class="overlay2"/>
28
+ <div class="overlay3"/>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup>
33
+ import SwDropdown from "@/components/SwDropdown.vue"
34
+ import SwButton from "@/components/SwButton.vue"
35
+ import {watchEffect, ref, onMounted} from "vue"
36
+ import SwIcon from "@/components/SwIcon.vue"
37
+ const swGide = ref(null)
38
+ const visual = ref(true)
39
+ const block = ref(null)
40
+ const step = defineModel({ type: Number, default: 0 })
41
+ const emit = defineEmits(['close'])
42
+ const props = defineProps({
43
+ steps: { type: Array, default: () => [] },
44
+ iconClose: { type: String, default: '' },
45
+ maxWidth: { type: Number, default: 250 }
46
+ })
47
+
48
+ const exitModule = () => emit('close')
49
+
50
+ const walk = (e) => {
51
+ const nextStep = step.value + e
52
+ if (nextStep < 0) step.value = 0
53
+ else if (nextStep >= props.steps.length) exitModule()
54
+ else step.value = nextStep
55
+ }
56
+
57
+ // Флаг: игнорировать следующее событие скролла (после автопрокрутки)
58
+ let ignoreNextScroll = false
59
+
60
+ // Функция прокрутки к элементу
61
+ const scrollToElement = (el) => {
62
+ const rect = el.getBoundingClientRect()
63
+ const viewportHeight = window.innerHeight
64
+
65
+ if (rect.top < 5) {
66
+ ignoreNextScroll = true
67
+ document.documentElement.scrollTop += rect.top - 5
68
+ } else if (rect.bottom > viewportHeight - 5) {
69
+ ignoreNextScroll = true
70
+ document.documentElement.scrollTop += rect.bottom - viewportHeight + 5
71
+ }
72
+ }
73
+
74
+ // Обновление позиции оверлея (без прокрутки!)
75
+ const update = () => {
76
+ if (!block.value || !swGide.value) return
77
+ const r = block.value.getBoundingClientRect()
78
+ const sT = window.pageYOffset || document.documentElement.scrollTop
79
+ const sL = window.pageXOffset || document.documentElement.scrollLeft
80
+
81
+ swGide.value.style.gridTemplateRows = `${r.top + sT - 5}px ${r.height + 10}px 1fr`
82
+ swGide.value.style.gridTemplateColumns = `${r.left + sL - 5}px ${r.width + 10}px 1fr`
83
+ swGide.value.style.height = document.documentElement.scrollHeight + 'px'
84
+ }
85
+ watchEffect((onInvalidate) => {
86
+ if (!swGide.value) return
87
+ const selector = props.steps[step.value]?.tag
88
+ if (!selector || !selector.trim()) {
89
+ // Сброс стилей, если нет селектора
90
+ swGide.value.style.gridTemplateRows = `40% 0 1fr`
91
+ swGide.value.style.gridTemplateColumns = `1fr 0 1fr`
92
+ swGide.value.style.height = document.documentElement.scrollHeight + 'px'
93
+ return
94
+ }
95
+ block.value = document.querySelector(selector)
96
+ if (!block.value) {
97
+ console.warn(`[SwGuide] Элемент не найден: ${selector}`)
98
+ return
99
+ }
100
+ // === Автопрокрутка при смене шага ===
101
+ // Выполняем один раз после установки нового блока
102
+ scrollToElement(block.value)
103
+ // === Обновление оверлея при ресайзе/скролле ===
104
+ const onResizeOrScroll = () => {
105
+ if (ignoreNextScroll) {
106
+ ignoreNextScroll = false // сброс флага
107
+ return // игнорируем "нашу" прокрутку
108
+ }
109
+ update()
110
+ }
111
+ // Инициализация
112
+ update()
113
+ // Наблюдатели
114
+ const observer = new ResizeObserver(update)
115
+ observer.observe(block.value)
116
+
117
+ window.addEventListener('resize', onResizeOrScroll)
118
+ window.addEventListener('scroll', onResizeOrScroll, { passive: true })
119
+
120
+ // Очистка
121
+ onInvalidate(() => {
122
+ observer.disconnect()
123
+ window.removeEventListener('resize', onResizeOrScroll)
124
+ window.removeEventListener('scroll', onResizeOrScroll)
125
+ })
126
+ })
127
+ onMounted(()=> window.onload = () => block.value = document.querySelector(props.steps[step.value]?.tag))
128
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <div v-if="isExt" :style="'mask: url(' + props.iconClass + ') no-repeat 50% 50%;'" class="sw-external-icon svg-icon" v-bind="$attrs" />
3
+ <svg v-else :class="'sw-icon icon-' + (props.className || props.iconClass)" aria-hidden="true" v-bind="$attrs">
4
+ <use :href="'#' + props.prefix +'-' + props.iconClass" />
5
+ </svg>
6
+ </template>
7
+ <script setup>
8
+ import { isExternal } from '@/utils/index.js'
9
+
10
+ const props = defineProps({
11
+ prefix: {type: String, default: 'icon'},
12
+ iconClass: {type: String, required: true},
13
+ className: {type: String, default: ''}
14
+ })
15
+ const isExt = isExternal(props.iconClass)
16
+ </script>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from "vue"
3
+ import SwIcon from "@/components/SwIcon.vue"
4
+ import { formatPhoneNumber } from "@/utils/index"
5
+
6
+ // Define prop types - separate model from props
7
+ interface InputProps {
8
+ before?: string
9
+ after?: string
10
+ placeholder?: string
11
+ label?: string
12
+ name: string
13
+ class?: string
14
+ size?: string
15
+ type?: string
16
+ inputMode?: string
17
+ required?: boolean
18
+ maxlength?: number
19
+ minlength?: number
20
+ inputSize?: number
21
+ autofocus?: boolean
22
+ }
23
+
24
+ // Use defineModel for the input value
25
+ const modelValue = defineModel<string>()
26
+
27
+ // Define props separately
28
+ const props = withDefaults(defineProps<InputProps>(), {
29
+ before: '',
30
+ after: '',
31
+ placeholder: '',
32
+ label: '',
33
+ class: '',
34
+ size: '',
35
+ type: 'text',
36
+ inputMode: 'text',
37
+ required: false,
38
+ maxlength: 128,
39
+ minlength: 1,
40
+ inputSize: 60,
41
+ autofocus: false
42
+ })
43
+
44
+ const lab = ref<HTMLLabelElement | null>(null)
45
+ const emit = defineEmits(['suffix','prefix','focusInput'])
46
+ const onfocus = (e: FocusEvent) => {
47
+ // console.log(e)
48
+ emit('focusInput')
49
+ const target = e.target as HTMLInputElement
50
+ target.setSelectionRange(modelValue.value?.length || 0, modelValue.value?.length || 0)
51
+ if(lab.value !== null) lab.value.style.top = '-.6em'
52
+ }
53
+
54
+ const onblur = (e: FocusEvent) => {
55
+ if((modelValue.value?.length || 0) === 0 && lab.value !== null) lab.value.removeAttribute("style")
56
+ }
57
+
58
+ // Set initial label position if there's a value
59
+ if((modelValue.value?.length || 0) > 0 && lab.value !== null) lab.value.style.top = '-.6em'
60
+
61
+ const suffix = ()=>{ emit('suffix') }
62
+ const prefix = ()=>{ emit('prefix') }
63
+ const style = computed(() =>{
64
+ let s = ['sw-input']
65
+ if(props.size.length > 0) s.push('sw-' + props.size)
66
+ if(props.class.length > 0) s.push(props.class)
67
+ return s
68
+ }
69
+ )
70
+ watch(modelValue, (newValue, oldValue) => {
71
+ if(props.type === 'phone' && newValue) {
72
+ modelValue.value = formatPhoneNumber(newValue, oldValue)
73
+ }
74
+ })
75
+ </script>
76
+
77
+ <template>
78
+ <div :class="style">
79
+ <sw-icon v-if="props.before.length > 0" :icon-class="props.before" @click="prefix"/>
80
+ <slot v-else name="prefix"></slot>
81
+ <label v-if="(props.label.length > 0)" :for="props.name" ref="lab">{{ props.label }}<span title="Это поле обязательно для заполнения." v-if="props.required">*</span></label>
82
+ <input
83
+ v-model="model"
84
+ :required="props.required"
85
+ :placeholder="(props.required && props.label.length === 0)? props.placeholder + `*`: props.placeholder"
86
+ :type="props.type"
87
+ :id="props.name"
88
+ :name="props.name"
89
+ :size="props.inputSize"
90
+ @focus="onfocus"
91
+ @blur="onblur"
92
+ :maxlength="props.maxlength"
93
+ :minlength="props.minlength"
94
+ :autofocus="props.autofocus"
95
+ v-bind="$attrs"
96
+ >
97
+ <sw-icon v-if="props.after.length > 0" :icon-class="props.after" @click="suffix"/>
98
+ <slot v-else name="suffix"></slot>
99
+ </div>
100
+ </template>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <dialog :class="'sw-message ' + props.class" ref="messageRef" >
3
+ <header v-if="props.name.length > 0 || !!slots?.header" ><slot name="header">{{ props.name }} <sw-button link type="primary" @click="model = false"><sw-icon :icon-class="props.iconAfter" /></sw-button></slot></header>
4
+ <slot></slot>
5
+ <footer><slot name="footer"></slot></footer>
6
+ </dialog>
7
+ </template>
8
+
9
+ <script setup>
10
+ import {nextTick, onUnmounted, ref, watch, useSlots} from "vue"
11
+ import SwButton from "@/components/SwButton.vue"
12
+ import SwIcon from "@/components/SwIcon.vue"
13
+
14
+ const model = defineModel()
15
+ const props = defineProps({
16
+ name: {type: String, default: ''},
17
+ class: {type:String, default: ''},
18
+ iconAfter: {type:String, default: ''}
19
+ })
20
+ const messageRef = ref(null)
21
+ const slots = useSlots();
22
+ const handleClickOutside = (e) => {
23
+ if (messageRef.value && model.value) {
24
+ const dialogRect = messageRef.value.getBoundingClientRect()
25
+ const isClickInsideDialog = (
26
+ e.clientX >= dialogRect.left &&
27
+ e.clientX <= dialogRect.right &&
28
+ e.clientY >= dialogRect.top &&
29
+ e.clientY <= dialogRect.bottom
30
+ )
31
+
32
+ // Если клик вне диалога, закрываем его
33
+ if (!isClickInsideDialog) {
34
+ model.value = false
35
+ }
36
+ }
37
+ }
38
+ // Когда модель становится true — добавляем обработчик
39
+ watch(() => model.value, (newVal) => {
40
+ if (newVal) {
41
+ messageRef.value.showModal()
42
+
43
+ nextTick() // ждём, пока DOM обновится
44
+ window.addEventListener('click', handleClickOutside, true)
45
+ } else {
46
+ messageRef.value.close()
47
+ window.removeEventListener('click', handleClickOutside)
48
+ }
49
+ })
50
+ onUnmounted(() => {
51
+ window.removeEventListener('click', handleClickOutside)
52
+ })
53
+ </script>
@@ -0,0 +1,17 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ name: {type: String, default: ''},
4
+ class: {type:String, default: ''},
5
+ iconAfter: {type:String, default: ''}
6
+ })
7
+ const emit = defineEmits(['header'])
8
+ const header = () => {emit('header')}
9
+ </script>
10
+
11
+ <template>
12
+ <section :class="'sw-section ' + props.class">
13
+ <header @click="header"><slot name="header">{{ props.name }} <svg-icon :icon-class="props.iconAfter" /></slot></header>
14
+ <slot></slot>
15
+ <footer><slot name="footer"></slot></footer>
16
+ </section>
17
+ </template>
@@ -0,0 +1,151 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from "vue"
3
+ import axios from 'axios'
4
+
5
+ // Define types
6
+ interface AddressData {
7
+ fullText: string
8
+ }
9
+
10
+ interface Suggestion {
11
+ value: string
12
+ data: {
13
+ geo_lat: string
14
+ geo_lon: string
15
+ }
16
+ }
17
+
18
+ interface GeoData {
19
+ text: string
20
+ coords: [number, number]
21
+ suggestion: any
22
+ }
23
+
24
+ const address = ref<AddressData>({ fullText: '' })
25
+ const visible = ref<boolean>(false)
26
+ const options = ref<Suggestion[]>([])
27
+
28
+ const model = defineModel<GeoData>()
29
+
30
+ interface Props {
31
+ class?: string
32
+ size?: 'large' | 'small'
33
+ placeholder?: string
34
+ token?: string
35
+ }
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ class: '',
39
+ size: 'large',
40
+ placeholder: '',
41
+ token: ''
42
+ })
43
+ watch(() => model.value?.text, (newValue) => {
44
+ if (newValue && newValue.length > 3) address.value.fullText = newValue
45
+ })
46
+ watch(address.value.fullText,(newValue, oldValue) => {
47
+ visible.value = (newValue.length > 3 && newValue.length > oldValue.length)
48
+ if (newValue.length >3) getDadata()
49
+ })
50
+ const getDadata = () => {
51
+ let url='https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address'
52
+ console.log(address.value.fullText)
53
+ axios.post(url, {
54
+ query: address.value.fullText },
55
+ {
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ "Accept": "application/json",
59
+ "Authorization": 'Token ' + props.token
60
+ }
61
+ })
62
+ .then(response => {
63
+ console.log(response.data.suggestions)
64
+ options.value = response.data.suggestions
65
+ // this.visible = (this.options.length > 1)
66
+ if (options.value.length === 1) clickLi(options.value[0])
67
+ // this.geo.coords = [Number(response.data.result[0]), Number(response.data.result[1])]
68
+ })
69
+ .catch(error => {
70
+ // handle error
71
+ console.log(error);
72
+ })
73
+ }
74
+ const clickLi = (item) => {
75
+ address.value.fullText = item.value
76
+ visible.value = false
77
+ const geo = {}
78
+ geo.text = item.value
79
+ geo.coords = [Number(item.data.geo_lat), Number(item.data.geo_lon)]
80
+ geo.suggestion = item.data
81
+ model.value = geo
82
+ }
83
+ </script>
84
+
85
+ <template>
86
+ <div class="popup">
87
+ <sw-input
88
+ :size="props.size"
89
+ v-model="address.fullText"
90
+ :placeholder="props.placeholder"
91
+ />
92
+ <div class="wrapper" v-show="visible">
93
+ <ul class="scrollbar">
94
+ <li tabindex="1" v-for="(item, key) in options" :key="key" @click="clickLi(item)">{{ item.value }}</li>
95
+ </ul>
96
+ </div>
97
+ </div>
98
+ </template>
99
+
100
+ <style lang="scss">
101
+ .popup{
102
+ position: relative;
103
+ .wrapper{
104
+ position: absolute;
105
+ z-index: 1;
106
+ list-style: none;
107
+ font-size: .8rem;
108
+ line-height: 1.2;
109
+ word-break: break-word;
110
+ padding: 0;
111
+ margin: 10px 0 0;
112
+ :before{
113
+ content: '';
114
+ border-bottom-color: transparent !important;
115
+ border-left-color: transparent !important;
116
+ border: 1px solid var(--el-border-color-light);
117
+ background: var(--el-bg-color-overlay);
118
+ right: 50%;
119
+ top: -6px;
120
+ position: absolute;
121
+ width: 10px;
122
+ height: 10px;
123
+ z-index: 1;
124
+ transform: rotate(-45deg);
125
+ }
126
+ }
127
+ .scrollbar {
128
+ box-shadow: 0 0 0 1px var(--el-border-color);
129
+ border-radius: var(--el-border-radius-base);
130
+ background: #ffffff;
131
+ padding: .5rem 0;
132
+ margin: 0;
133
+ li{display: flex;
134
+ align-items: start;
135
+ white-space: nowrap;
136
+ list-style: none;
137
+ line-height: 22px;
138
+ padding: 5px 16px;
139
+ margin:0;
140
+ font-size: var(--sw-font-size);
141
+ color: var(--sw-text-color-secondary);
142
+ cursor:pointer;
143
+ outline: none;
144
+ &:not(.is-disabled):focus{
145
+ background: var(--el-dropdown-menuItem-hover-fill);
146
+ color:var(--el-dropdown-menuItem-hover-color);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ </style>