@studio-west/component-sw 0.11.10 → 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 +64 -31
  44. package/types/index.d.ts +83 -59
  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,67 @@
1
+ <script setup>
2
+ import {computed, nextTick, onMounted, onUnmounted, ref, watch} from "vue"
3
+
4
+ const model = defineModel()
5
+ const buttons = ref([])
6
+ const buttonGroup = ref(null)
7
+ const observer = ref(null)
8
+ const props = defineProps({
9
+ class: {type:String, default: ''},
10
+ vertical: {type:Boolean, default: false},
11
+ radio: {type:Boolean, default: false},
12
+ round: {type:Boolean, default: false},
13
+ })
14
+ const style = computed(() =>{
15
+ let s = ['sw-button-group']
16
+ if(props.vertical) s.push('sw-vertical')
17
+ if(props.round) s.push('sw-round')
18
+ if(props.class.length > 0) s.push(props.class)
19
+ return s
20
+ })
21
+
22
+ onMounted(async () => {
23
+ await new Promise(resolve => setTimeout(resolve, 0))
24
+ if (props.radio) findSlotRootElements()
25
+ if (buttonGroup.value) {
26
+ observer.value = new MutationObserver(() => {
27
+ if (props.radio) findSlotRootElements()
28
+ })
29
+ observer.value.observe(buttonGroup.value, {
30
+ childList: true,
31
+ subtree: true
32
+ })
33
+ }
34
+ })
35
+
36
+ onUnmounted(() => {
37
+ if (observer.value) {
38
+ observer.value.disconnect()
39
+ }
40
+ })
41
+
42
+ watch(model,() => {
43
+ if (props.radio) {
44
+ [].forEach.call(buttons.value, (button, id) => {
45
+ if (id === model.value) button.classList.remove('sw-text')
46
+ else button.classList.add('sw-text')
47
+ })
48
+ }
49
+ })
50
+
51
+ const findSlotRootElements = () => {
52
+ buttons.value = []
53
+ if (buttonGroup.value) buttons.value = buttonGroup.value.querySelectorAll('button, .sw-button')
54
+ if (props.radio) {
55
+ [].forEach.call(buttons.value, (button, id) => {
56
+ if (model.value !== id) button.classList.add('sw-text');
57
+ button.addEventListener('click', () => model.value = id)
58
+ })
59
+ }
60
+ }
61
+ </script>
62
+
63
+ <template>
64
+ <div :class="style" ref="buttonGroup">
65
+ <slot />
66
+ </div>
67
+ </template>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <div class="sw-collapse" :class="{ open: props.visual }" :style="{ maxHeight: contentHeight }" ref="collapse"><slot></slot></div>
3
+ </template>
4
+ <script setup>
5
+ /**
6
+ * visual Boolean set opened module
7
+ **/
8
+ import {computed, ref, onMounted, onBeforeUnmount, watch} from "vue"
9
+ onMounted(() => {contentHeight.value = '0px'})
10
+ const collapse = ref(null);
11
+ const props = defineProps({
12
+ visual: { type: Boolean, default: true }
13
+ })
14
+ watch(() => props.visual, () => updateHeight())
15
+ const contentHeight = ref("0px");
16
+
17
+ const updateHeight = () => {
18
+ if (collapse.value) {
19
+ contentHeight.value = props.visual ? collapse.value.scrollHeight + "px" : "0px"
20
+ }
21
+ }
22
+ let resizeObserver
23
+
24
+ onMounted(() => {
25
+ updateHeight()
26
+ if (collapse.value) {
27
+ resizeObserver = new ResizeObserver(updateHeight)
28
+ resizeObserver.observe(collapse.value)
29
+ }
30
+ })
31
+ onBeforeUnmount(() => {
32
+ if (resizeObserver) {
33
+ resizeObserver.disconnect()
34
+ }
35
+ });
36
+ </script>
@@ -0,0 +1,375 @@
1
+ <script setup>
2
+ import {computed, ref} from 'vue'
3
+ import { parseTime, parseDateString } from "@/utils/index.js"
4
+
5
+ const props = defineProps({
6
+ data: Object,
7
+ limitation: {
8
+ type: [Boolean, Array],
9
+ default: () => false
10
+ },
11
+ range: {
12
+ type: String,
13
+ default: 'solid'
14
+ }
15
+ })
16
+
17
+ const emit = defineEmits(['input'])
18
+ const year = ref(5)
19
+ // Текущие даты
20
+ const start = ref(new Date())
21
+ const end = ref(new Date())
22
+
23
+ // Год и месяц для отображения
24
+ const startYear = ref(new Date().getFullYear())
25
+ const startMonth = ref(new Date().getMonth())
26
+ const endYear = ref(new Date().getFullYear())
27
+ const endMonth = ref(new Date().getMonth())
28
+ const nextMonth = ref(new Date(startYear.value, startMonth.value +1, 1).getMonth())
29
+
30
+ // Дни и индексатор
31
+ const days = ref([])
32
+ const ind = ref(0)
33
+ const startId = ref(-1)
34
+ const endId = ref(-1)
35
+ const locale = ref(navigator.language || 'ru')
36
+ const iso = (/en|zh|ja|he/.test(locale.value)) ? 0 : 1
37
+ const months = computed(() => {
38
+ return Array.from({ length: 12 }, (_, i) => {
39
+ const date = new Date(2024, i)
40
+ return new Intl.DateTimeFormat(locale.value, { month: 'long' }).format(date)
41
+ })
42
+ })
43
+ const decade = ref(Math.floor(startYear.value / 10) * 10)
44
+ const decades = computed(() => {
45
+ return Array.from({length:12}, (_,i) => {
46
+ return decade.value + i - 1
47
+ })
48
+ })
49
+
50
+ // Инициализация стартовой и конечной даты
51
+ if (props.data?.startDate) {
52
+ start.value = new Date(parseDateString(props.data.startDate))
53
+ startYear.value = start.value.getFullYear()
54
+ startMonth.value = start.value.getMonth()
55
+ } else if(props.data?.firstDate){
56
+ startYear.value = new Date(parseDateString(props.data.firstDate)).getFullYear()
57
+ startMonth.value = new Date(parseDateString(props.data.firstDate)).getMonth()
58
+ nextMonth.value = new Date(startYear.value, startMonth.value +1, 1).getMonth()
59
+ }
60
+ if (props.data?.endDate && props.range === 'range') {
61
+ end.value = new Date(parseDateString(props.data.endDate))
62
+ endYear.value = end.value.getFullYear()
63
+ endMonth.value = end.value.getMonth()
64
+ }
65
+
66
+ // Построение календаря
67
+ const view = () => {
68
+ days.value = []
69
+ ind.value = 0
70
+ // console.log('view', year.value)
71
+ // Предыдущий месяц
72
+ const preMonth = new Date(startYear.value, startMonth.value, 0)
73
+ for (let i = preMonth.getDate() - preMonth.getDay() + iso; i <= preMonth.getDate(); i++) {
74
+ if (preMonth.getDay() !== 6 + iso) {
75
+ days.value.push({ day: i, month: preMonth.getMonth(), year: preMonth.getFullYear(), class: [] })
76
+ ind.value++
77
+ }
78
+ }
79
+
80
+ // Текущий месяц
81
+ const currentMonthDays = new Date(startYear.value, startMonth.value + 1, 0).getDate()
82
+ for (let i = 1; i <= currentMonthDays; i++) {
83
+ days.value.push({ day: i, month: startMonth.value, year: startYear.value, class: [] })
84
+ ind.value++
85
+ }
86
+
87
+ // Следующий месяц (если диапазон)
88
+ if (props.range === 'range') {
89
+ const nextMonthDate = new Date(startYear.value, startMonth.value + 1, 1)
90
+ const nextMonthLastDay = new Date(startYear.value, startMonth.value + 2, 0).getDate()
91
+ for (let i = 1; i <= nextMonthLastDay; i++) {
92
+ days.value.push({ day: i, month: nextMonthDate.getMonth(), year: nextMonthDate.getFullYear(), class: [] })
93
+ }
94
+ }
95
+
96
+ // Подсветка праздников
97
+ highlightSpecialDays()
98
+ }
99
+
100
+ // Подсветка праздников и выходных
101
+ const highlightSpecialDays = () => {
102
+ if (!props.data?.holiday) return
103
+
104
+ const markClass = (dates, className) => {
105
+ dates.forEach(d => {
106
+ const [year, month, day] = d.split('-').map(Number)
107
+ const index = days.value.findIndex(el => el.year === year && el.month + 1 === month && el.day === day)
108
+ if (index !== -1) days.value[index].class[0] = className
109
+ })
110
+ }
111
+
112
+ markClass(props.data.holiday.preholidays, 'preholiday')
113
+ markClass(props.data.holiday.holidays, 'weekend')
114
+ markClass(props.data.holiday.holiday, 'holiday')
115
+ markClass(props.data.holiday.nowork, 'nowork')
116
+ }
117
+
118
+ // Проверка пересечения диапазонов
119
+ const checkOverlap = () => {
120
+ if (!props.limitation || props.limitation.length < 2) return false
121
+
122
+ const sorted = [...props.limitation].sort((a, b) =>
123
+ new Date(parseDateString(a.startDate)).getTime() - new Date(parseDateString(b.startDate)).getTime()
124
+ )
125
+
126
+ for (let i = 1; i < sorted.length; i++) {
127
+ const prevEnd = new Date(parseDateString(sorted[i - 1].endDate))
128
+ const currStart = new Date(parseDateString(sorted[i].startDate))
129
+ if (currStart <= prevEnd) {
130
+ console.warn("Warning: Диапазоны пересекаются:", sorted[i - 1], sorted[i])
131
+ return true
132
+ }
133
+ }
134
+ return false
135
+ }
136
+
137
+ // Фильтр дат по ограничениям
138
+ const filterData = (year, month, day) => {
139
+ if (!props.limitation || props.limitation.length === 0) return true
140
+
141
+ const targetDate = new Date(year, month, day).getTime()
142
+
143
+ for (let range of props.limitation) {
144
+ const { startDate, endDate } = range
145
+
146
+ // Если только endDate
147
+ if (!startDate && endDate) {
148
+ const end = new Date(parseDateString(endDate)).getTime()
149
+ if (targetDate <= end) return false
150
+ }
151
+
152
+ // Если только startDate
153
+ if (startDate && !endDate) {
154
+ const start = new Date(parseDateString(startDate)).getTime()
155
+ if (targetDate >= start) return false
156
+ }
157
+
158
+ // Если обе даты
159
+ if (startDate && endDate) {
160
+ const start = new Date(parseDateString(startDate)).getTime()
161
+ const end = new Date(parseDateString(endDate)).getTime()
162
+
163
+ if (start <= end) {
164
+ // обычный диапазон и одиночный
165
+ if (targetDate >= start && targetDate <= end) return false
166
+ } else {
167
+ // инвертированный диапазон (end < start)
168
+ if (targetDate >= start || targetDate <= end) return false
169
+ }
170
+ }
171
+ }
172
+ return true
173
+ }
174
+
175
+ // Выбор дня
176
+ const selectDays = (day, key) => {
177
+ const d = parseTime(new Date(day.year, day.month, day.day), '{dd}.{m}.{Y}')
178
+
179
+ if (props.range === 'range') {
180
+ if (endId.value !== -1) {
181
+ resetSelection()
182
+ startId.value = key
183
+ days.value[key].class[1] = 'active'
184
+ } else {
185
+ if (startId.value !== -1) {
186
+ endId.value = key
187
+ highlightRange(startId.value, endId.value, d)
188
+ } else {
189
+ startId.value = key
190
+ days.value[key].class[1] = 'active'
191
+ }
192
+ }
193
+ } else {
194
+ if (startId.value !== -1) {
195
+ days.value[startId.value].class[1] = ''
196
+ }
197
+ startId.value = key
198
+ days.value[key].class[1] = 'active'
199
+ emit('input', { dateStart: d, count: 1 })
200
+ }
201
+ }
202
+
203
+ // Очистка выделения
204
+ const resetSelection = () => {
205
+ days.value.forEach(day => day.class[1] = '')
206
+ startId.value = -1
207
+ endId.value = -1
208
+ }
209
+
210
+ // Подсветка диапазона
211
+ const highlightRange = (startKey, endKey, endDate) => {
212
+ let m = parseTime(new Date(days.value[startKey].year, days.value[startKey].month, days.value[startKey].day), '{dd}.{m}.{Y}')
213
+ if (endKey >= startKey) {
214
+ for (let i = startKey + 1; i < endKey; i++) {
215
+ days.value[i].class[1] = 'select'
216
+ }
217
+ emit('input', {dateStart: m, dateEnd: endDate, count: countDays(startKey, endKey)})
218
+ } else {
219
+ for (let i = endKey + 1; i < startKey; i++) {
220
+ days.value[i].class[1] = 'select'
221
+ }
222
+ emit('input', {dateStart: endDate, dateEnd: m, count: countDays(endKey, startKey)})
223
+ }
224
+ days.value[endKey].class[1] = 'active'
225
+ }
226
+
227
+ // Подсчет рабочих дней
228
+ const countDays = (a, b) => {
229
+ let count = Math.abs(a - b) + 1
230
+ let [min, max] = [Math.min(a, b), Math.max(a, b)]
231
+ for (let i = min; i <= max; i++) {
232
+ if (days.value[i]?.class[0] === 'holiday') count--
233
+ }
234
+ return count
235
+ }
236
+
237
+ // Изменение месяца
238
+ const change = (years, month) => {
239
+ if (filterData(years, month)) {
240
+ start.value = new Date(years, month, 1)
241
+ startYear.value = start.value.getFullYear()
242
+ startMonth.value = start.value.getMonth()
243
+ nextMonth.value = new Date(years, month + 1, 1).getMonth()
244
+ // console.log("change", year.value)
245
+ if((props.range === 'year' || props.range === 'decades') && year.value === 0) selectDays({year:years, month, day:1},8)
246
+ else view()
247
+ }
248
+ }
249
+
250
+ // Инициализация
251
+ view()
252
+
253
+ // Проверка на пересечение диапазонов
254
+ checkOverlap()
255
+
256
+ // Установка активных дат
257
+ if (props.data?.startDate) {
258
+ startId.value = days.value.findIndex(el => (
259
+ el.day === start.value.getDate() &&
260
+ el.month === startMonth.value &&
261
+ el.year === startYear.value
262
+ ))
263
+ if (startId.value !== -1) days.value[startId.value].class[1] = 'active'
264
+ }
265
+
266
+ if (props.data?.endDate && props.range === 'range') {
267
+ endId.value = days.value.findIndex(el => (
268
+ el.day === end.value.getDate() &&
269
+ el.month === endMonth.value &&
270
+ el.year === endYear.value
271
+ ))
272
+ if (endId.value !== -1) {
273
+ days.value[endId.value].class[1] = 'active'
274
+ for (let i = startId.value + 1; i < endId.value; i++) {
275
+ days.value[i].class[1] = 'select'
276
+ }
277
+ }
278
+ }
279
+ </script>
280
+
281
+ <template>
282
+ <div class="sw-data-picker">
283
+ <div>
284
+ <p class="month_year" v-if="props.range ==='decades' || year === 3">
285
+ <span>
286
+ <button @click.prevent="decade -= 10;change(startYear,startMonth )"><slot name="double-arrow">«</slot></button>
287
+ </span>
288
+ <span class="text">{{decades[0]}} - {{decades[11]}}</span>
289
+ <span>
290
+ <button class="sw-revers" @click.prevent="decade += 10;change(startYear,startMonth )"><slot name="double-arrow">«</slot></button>
291
+ </span>
292
+ </p>
293
+ <p class="month_year" v-else>
294
+ <span>
295
+ <button @click.prevent="change(startYear - 1,startMonth)"><slot name="double-arrow">«</slot></button>
296
+ <button @click.prevent="year = 0;change(startYear,startMonth - 1)"><slot name="arrow">‹</slot></button>
297
+ </span>
298
+ <span class="text">
299
+ <button @click.prevent="year = 3">{{ startYear }}</button>
300
+ <button @click.prevent="year = 1">{{ parseTime(start.setMonth(startMonth), '{F}') }}</button>
301
+ </span>
302
+ <span>
303
+ <button class="sw-revers" @click.prevent="year = 0;change(startYear,startMonth + 1)"><slot name="arrow">‹</slot></button>
304
+ <button class="sw-revers" @click.prevent="change(startYear + 1,startMonth)"><slot name="double-arrow">«</slot></button>
305
+ </span>
306
+ </p>
307
+ <div class="week" v-if="!(props.range ==='year' || props.range ==='decades' || year === 1 || year === 3)">
308
+ <button v-for="(i, key) in 7" :key="key" class="disabled">{{ parseTime(new Date(1970,1,i + iso), '{D}') }}</button>
309
+ </div>
310
+ <div class="year" v-if="year !== 3 && props.range ==='year' || year === 1">
311
+ <template v-for="(month, index) in months" :key="index">
312
+ <button @click.prevent="year = 0;change(startYear,index)" :class="(index === startMonth)?'active' :''">{{ month }}</button>
313
+ </template>
314
+ </div>
315
+ <div class="year" v-else-if="props.range ==='decades' || year === 3">
316
+ <template v-for="(y, ind) in decades" :key="ind">
317
+ <button @click.prevent="year = (props.range ==='year')?1:0;change(y,startMonth)" :class="(decade + ind - 1 === startYear)?'active' :''">{{ y }}</button>
318
+ </template>
319
+ </div>
320
+ <div class="month" v-else>
321
+ <template v-for="(day, key) in days" :key="key">
322
+ <button v-if="key < ind + (7 - new Date(startYear,startMonth +1, 1).getDay() + iso) % 7"
323
+ :class="(day.month === startMonth && filterData(day.year, day.month, day.day))? day.class : 'disabled'"
324
+ @click.prevent="() => {if(day.month === startMonth) selectDays(day,key)}">{{ day.day }}</button>
325
+ </template>
326
+ </div>
327
+ </div>
328
+ <div v-if="props.range === 'range'">
329
+ <p class="month_year" v-if="props.range ==='decades' || year === 4">
330
+ <span>
331
+ <button @click.prevent="decade -= 10;change(startYear,startMonth )"><slot name="double-arrow">«</slot></button>
332
+ </span>
333
+ <span class="text">{{decades[0]}} - {{decades[11]}}</span>
334
+ <span>
335
+ <button class="sw-revers" @click.prevent="decade += 10;change(startYear,startMonth )"><slot name="double-arrow">«</slot></button>
336
+ </span>
337
+ </p>
338
+ <p class="month_year" v-else>
339
+ <span>
340
+ <button @click.prevent="change(startYear - 1,startMonth)"><slot name="double-arrow">«</slot></button>
341
+ <button @click.prevent="year = 0;change(startYear,startMonth - 1)"><slot name="arrow">‹</slot></button>
342
+ </span>
343
+ <span class="text">
344
+ <button @click.prevent="year = 4">{{ new Date(startYear, (startMonth + 1)).getFullYear()}}</button>
345
+ <button @click.prevent="year = 2">{{ parseTime(start.setMonth(startMonth +1), '{F}') }}</button>
346
+ </span>
347
+ <span>
348
+ <button class="sw-revers" @click.prevent="year = 0;change(startYear,startMonth + 1)"><slot name="arrow">‹</slot></button>
349
+ <button class="sw-revers" @click.prevent="change(startYear + 1,startMonth)"><slot name="double-arrow">«</slot></button>
350
+ </span>
351
+ </p>
352
+ <div class="week" v-if="!(year === 2 || year === 4)">
353
+ <button v-for="(i, key) in 7" :key="key" class="disabled">{{ parseTime(new Date(1970,1,i + iso), '{D}') }}</button>
354
+ </div>
355
+ <div class="year" v-if="year === 2">
356
+ <template v-for="(month, index) in months" :key="index">
357
+ <button @click.prevent="year = 0;change(startYear,index-1)" :class="(index === (startMonth + 1) % 12 )?'active' :''">{{ month }}</button>
358
+ </template>
359
+ </div>
360
+ <!-- decades -->
361
+ <div class="year" v-else-if="year === 4">
362
+ <template v-for="(y, ind) in decades" :key="ind">
363
+ <button @click.prevent="year = 0;change(y,startMonth)" :class="(decade + ind - 1 === startYear)?'active' :''">{{ y }}</button>
364
+ </template>
365
+ </div>
366
+ <div class="month" v-else>
367
+ <template v-for="(day, key) in days" :key="key">
368
+ <button v-if="key >= ind - (7 + new Date(startYear,startMonth +1, 1).getDay() - iso) % 7"
369
+ :class="(day.month === nextMonth && filterData(day.year, day.month, day.day))? day.class :'disabled'"
370
+ @click.prevent="() => {if(day.month === nextMonth) selectDays(day,key)}">{{ day.day }}</button>
371
+ </template>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </template>
@@ -0,0 +1,202 @@
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
+ ref="popupRef"
14
+ :id ="id"
15
+ class="sw-dropdown-popup"
16
+ :class="props.class"
17
+ v-if="manual || false"
18
+ :style="popupStyle"
19
+ tabindex="-1"
20
+ >
21
+ <slot name="dropdown"></slot>
22
+ </ul>
23
+ </Teleport>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ /**
29
+ * class String add class css to popup
30
+ * size String 'large' | 'small'
31
+ * type String 'primary' | 'success' | 'info' | 'warning' | 'danger'
32
+ * trigger String 'click' | 'hover' | 'context' | 'none'
33
+ * placement String 'bottom-left' | 'bottom' | 'bottom-right' | 'top-left' | 'top' | 'top-right' | 'left' | 'right'
34
+ **/
35
+ import { computed, ref, onMounted, onUnmounted, watchEffect } from "vue"
36
+
37
+ const props = defineProps({
38
+ class: {type: String, default: ''},
39
+ size: {type: String, default: ''},
40
+ type: {type: String, default: ''},
41
+ trigger: {type: String, default: "click"},
42
+ placement: {type: String, default: 'bottom-left'},
43
+ maxWidth: {type: Number, default: 0}
44
+ })
45
+ const manual = defineModel()
46
+
47
+ const dropdownRef = ref(null)
48
+ const popupRef = ref(null)
49
+ const popupStyle = ref({})
50
+ const id = Math.ceil(Math.random() * 1000)
51
+ const repositionTrigger = ref(0)
52
+ const style = computed(() =>{
53
+ let s = ['sw-dropdown']
54
+ if(props.size.length > 0) s.push('sw-' + props.size)
55
+ if(props.type.length > 0) s.push('sw-' + props.type)
56
+ return s
57
+ }
58
+ )
59
+ const resizeObserver = new ResizeObserver(() => {
60
+ if (manual.value) repositionTrigger.value++
61
+ if(repositionTrigger.value > 99 ) repositionTrigger.value = 1
62
+ })
63
+
64
+ watchEffect( () => {
65
+ // console.log('watchEffect', repositionTrigger.value)
66
+ // Если popup не активен — ничего не делаем
67
+ if (!manual.value || !popupRef.value || !dropdownRef.value) {
68
+ // popupStyle.value = {}
69
+ return
70
+ }
71
+ if(repositionTrigger.value < 2) resizeObserver.observe(popupRef.value)
72
+ // repositionTrigger.value
73
+ let scrollTop = window.pageYOffset || document.documentElement.scrollTop
74
+ let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
75
+ document.body.style.position = 'fixed'
76
+ document.body.style.top = `-${scrollTop}px`
77
+ document.body.style.left = `-${scrollLeft}px`
78
+ document.body.style.position = ''
79
+ document.body.style.top = ''
80
+ document.body.style.left = ''
81
+ document.documentElement.scrollTop = scrollTop
82
+ document.documentElement.scrollLeft = scrollLeft
83
+ const buttonRect = dropdownRef.value.getBoundingClientRect()
84
+ let viewportWidth = window.innerWidth
85
+ let viewportHeight = window.innerHeight
86
+ // Получаем высоту и ширину popup
87
+ let popupHeight = popupRef.value.offsetHeight;
88
+ let popupWidth = popupRef.value.offsetWidth;
89
+ // Расчет позиции popup (например, снизу кнопки)
90
+ // Центр по горизонтали
91
+ let left = buttonRect.left + (buttonRect.width / 2) + scrollLeft
92
+ // Центр по вертикали
93
+ let top = buttonRect.top + (buttonRect.height / 2) + scrollTop
94
+ let deltaTop = (buttonRect.height / 2) + 8
95
+ const [mainPlacement, subPlacement] = props.placement.split('-', 2)
96
+
97
+ // Основное позиционирование
98
+ switch (mainPlacement) {
99
+ case "bottom":
100
+ top = (scrollTop + viewportHeight - popupHeight - deltaTop - 8 < top)? top - deltaTop - popupHeight : top + deltaTop
101
+ if(subPlacement === undefined ) left -= popupWidth / 2
102
+ break
103
+ case "top":
104
+ top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
105
+ if(subPlacement === undefined ) left -= popupWidth / 2
106
+ break
107
+ case "left":
108
+ if(left - popupWidth - (buttonRect.width / 2) - 8 < 0) {
109
+ if(left + (buttonRect.width / 2) + 8 + popupWidth < viewportWidth) { // справа
110
+ left = left + (buttonRect.width / 2) + 8
111
+ top = top - (popupHeight / 2)
112
+ } else {
113
+ top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
114
+ left = (left + (buttonRect.width / 2) < viewportWidth )? left + (buttonRect.width / 2) - popupWidth: left - (popupWidth / 2)
115
+ // left = ( buttonRect.width / 2 > popupWidth) ? left + 8 : left + 8 - (buttonRect.width / 2)
116
+ }
117
+ } else { // слева
118
+ left = left - popupWidth - (buttonRect.width / 2) - 8
119
+ top = top - (popupHeight / 2)
120
+ }
121
+ break
122
+ case "right":
123
+ if(left + (buttonRect.width / 2) + 8 + popupWidth > viewportWidth ) {
124
+ if(left - popupWidth - (buttonRect.width / 2) - 8 > 0){
125
+ left = left - popupWidth - (buttonRect.width / 2) - 8 // слева
126
+ top = top - (popupHeight / 2)
127
+ } else {
128
+ top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
129
+ // left = (scrollLeft + viewportWidth - 8 > left - (buttonRect.width / 2) + popupWidth) ? left - (buttonRect.width / 2) : left - popupWidth + (buttonRect.width / 2)
130
+ left = left - (buttonRect.width / 2) // влево
131
+ }
132
+ } else { // справа
133
+ left = left + (buttonRect.width / 2) + 8
134
+ top = top - (popupHeight / 2)
135
+ }
136
+ break
137
+ }
138
+ switch (subPlacement) {
139
+ case 'left':
140
+ left = (scrollLeft + viewportWidth - 8 > left - (buttonRect.width / 2) + popupWidth) ? left - (buttonRect.width / 2) : left - popupWidth + (buttonRect.width / 2)
141
+ break
142
+ case 'right':
143
+ left = (8< left + (buttonRect.width / 2) - popupWidth) ? left - popupWidth + (buttonRect.width / 2) : left - (buttonRect.width / 2)
144
+ break
145
+ }
146
+ // Убедимся, что попап не выходит за экран
147
+ // if (left + popupWidth > viewportWidth) left = viewportWidth - popupWidth
148
+ if (left < 0) left = 0
149
+ // if (top + popupHeight > viewportHeight) top = viewportHeight - popupHeight
150
+ if (top < 0) top = 0
151
+
152
+ popupStyle.value = {
153
+ position: 'absolute',
154
+ top: `${top}px`,
155
+ left: `${left}px`,
156
+ maxWidth: props.maxWidth > 0 ? `${props.maxWidth}px` : ''
157
+ }
158
+ }, {flush: 'post'}
159
+ )
160
+
161
+ const toggleClick = () => {
162
+ if(props.trigger === 'click') manual.value = !manual.value
163
+ //if(!manual.value) console.log("toggleClick")
164
+ }
165
+ const toggleHover = () => {
166
+ if(props.trigger === 'hover') manual.value = !manual.value
167
+ //if(!manual.value) console.log("toggleHover")
168
+ }
169
+ const toggleContext = () => {
170
+ if (props.trigger === 'context') manual.value = !manual.value
171
+ //if(!manual.value) console.log("toggleContext")
172
+ }
173
+ const handleResize = () => {
174
+ if (manual.value) repositionTrigger.value++
175
+ if(repositionTrigger.value > 99 ) repositionTrigger.value = 1
176
+ // console.log("positionPopup", repositionTrigger.value)
177
+ }
178
+ defineExpose({ handleResize })
179
+ const handleClickOutside = (e) => {
180
+ if(props.trigger !=='none' && !e.target.closest(`[id="${id}"]`) && (dropdownRef.value && !dropdownRef.value.contains(e.target))) manual.value = false
181
+ //if(!manual.value) console.log("Outside", dropdownRef.value.contains(e.target))
182
+ }
183
+ onMounted(() => {
184
+ //console.log("onMounted")
185
+ window.addEventListener("resize", handleResize)
186
+ if (document.documentElement.ontouchstart !== undefined) document.documentElement.addEventListener("touchstart", handleClickOutside, true)
187
+ else document.documentElement.addEventListener("click", handleClickOutside, true)
188
+ if (window.onscrollend !== undefined) window.addEventListener("scrollend", handleResize)
189
+ else window.addEventListener("scroll", handleResize)
190
+ })
191
+
192
+ onUnmounted(() => {
193
+ //console.log("onUnmounted")
194
+ resizeObserver.disconnect()
195
+ window.removeEventListener("resize", handleResize)
196
+ if (document.documentElement.ontouchstart !== undefined) document.documentElement.removeEventListener("touchstart", handleClickOutside)
197
+ else document.documentElement.removeEventListener("click", handleClickOutside)
198
+ if (window.onscrollend !== undefined) window.removeEventListener("scrollend", handleResize)
199
+ else window.removeEventListener("scroll", handleResize)
200
+ })
201
+
202
+ </script>