@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.
- package/README.md +5 -5
- package/dist/SwButton-yS_tKW9w.js +4 -0
- package/dist/{SwButton-jKDKwFV9.js → SwButton.vue_vue_type_script_setup_true_lang-aODPwFa6.js} +11 -11
- package/dist/{SwDatePicker-CJjKSM8k.js → SwDatePicker-CpmdOhuc.js} +1 -1
- package/dist/{SwDropdownItem-CtlMVgsX.js → SwDropdownItem-BE6ZRWT1.js} +1 -1
- package/dist/{SwGide-DbSSyZ-y.js → SwGide-_a5-3g_f.js} +2 -2
- package/dist/SwInput-CbNd7Vin.js +90 -0
- package/dist/{SwMessage-CovKkpf6.js → SwMessage-DdUbYQet.js} +6 -6
- package/dist/SwSection-CQe2kE0O.js +34 -0
- package/dist/SwSelect-BxbCfof-.js +1883 -0
- package/dist/{SwSlider-YncjYKPw.js → SwSlider-jWTzzPZg.js} +1 -1
- package/dist/SwSwitch-DeMdyD0-.js +47 -0
- package/dist/index-C3iiqwEz.js +188 -0
- package/dist/index.cjs +6 -1
- package/dist/index.js +1 -1
- package/package.json +6 -2
- package/src/Alert.ts +65 -0
- package/src/components/SwAlert.vue +70 -0
- package/src/components/SwButton.vue +50 -0
- package/src/components/SwButtonGroup.vue +67 -0
- package/src/components/SwCollapse.vue +36 -0
- package/src/components/SwDatePicker.vue +375 -0
- package/src/components/SwDropdown.vue +202 -0
- package/src/components/SwDropdownItem.vue +26 -0
- package/src/components/SwDropdownNew.vue +175 -0
- package/src/components/SwFormItem.vue +21 -0
- package/src/components/SwGide.vue +128 -0
- package/src/components/SwIcon.vue +16 -0
- package/src/components/SwInput.vue +100 -0
- package/src/components/SwMessage.vue +53 -0
- package/src/components/SwSection.vue +17 -0
- package/src/components/SwSelect.vue +151 -0
- package/src/components/SwSkeleton.vue +13 -0
- package/src/components/SwSkeletonItem.vue +27 -0
- package/src/components/SwSlider.vue +281 -0
- package/src/components/SwSwitch.vue +51 -0
- package/src/components/SwTable.vue +239 -0
- package/src/components/SwTableColumn.vue +25 -0
- package/src/components/SwTabs.vue +41 -0
- package/src/components/SwTabsPane.vue +44 -0
- package/src/index.ts +43 -0
- package/src/utils/index.ts +149 -0
- package/types/components.d.ts +64 -31
- package/types/index.d.ts +83 -59
- package/dist/SwInput-DCV1rrWa.js +0 -89
- package/dist/SwSection-D8ooQ21I.js +0 -37
- package/dist/SwSelect-C2RKinez.js +0 -72
- package/dist/SwSwitch-6rl1IT4p.js +0 -47
- 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>
|