진스
Vue Swipe Tabs 구현 본문
728x90
탭을 스와이프해서 넘기는 것을 구현하고자 한다.
vuetify 라이브러리를 사용 할수도 있지만 세부 커스터 마이징이 어려워서 라이브러리를 쓰진 않았다.
방법1.
HTML
<!-- TAB content -->
<div v-on="pointer
?{pointerdown: ($event)=>start($event), pointermove: ($event)=>move($event), pointerup: ($event)=>end($event)}
:{touchstart: ($event)=>start($event), touchmove: ($event)=>move($event), touchend: ($event)=>end($event)}"
style="touch-action: pan-y;"
>
<transition :name="transition" v-for="(tab, idx) in tabs" :key="idx">
<div v-if="idx === activetab">
- 이벤트 캐치할 div
- 탭의 개수만큼 Transition 생성
- 선택된 탭만 보이도록 하위 div에서 v-if 조건으로 설정한다.
JS
/* TAB 관련 Method */
switchtab (n) {
if (this.activetab > n) {
this.transition = 'slide-prev'
} else if (this.activetab < n) {
this.transition = 'slide-next'
}
},
start (e) {
this.settouchpos(e, 'start')
},
move (e) {
this.settouchpos(e, 'move')
},
end (e) {
this.settouchpos(e, 'end')
let dx = this.touch.sx - this.touch.ex,
dy = this.touch.sy - this.touch.ey,
dt = this.touch.et - this.touch.st,
dir = Math.sign(dx),
ntab = this.activetab + dir,
vmove = Math.abs(dx) / Math.abs(dy) < Math.sqrt(4)
ntab = ntab >= 0 && ntab < this.tabs.length ? ntab : null
if (Math.abs(dx) > 10 && ntab !== null && !vmove && dt < 300) this.switchtab(ntab)
},
settouchpos (e, event) {
let ev = e.changedTouches ? e.changedTouches[0] : e
if (event === 'start') {
this.touch.sx = ev.clientX
this.touch.sy = ev.clientY
this.touch.st = Date.now()
} else {
this.touch.ex = ev.clientX
this.touch.ey = ev.clientY
this.touch.et = Date.now()
}
}
- Switchtab : 탭 변경함수
- Start : 터치 시작
- Move : 터치 이동
- End : 터치 끝 ( 스와이프 방향에 따라 switchtab 함수 호출)
- Settouchpos : start, end 좌표를 기록하는 함수
CSS
.slide-next-leave-active, .slide-next-enter-active, .slide-prev-enter-active, .slide-prev-leave-active {
transition: .3s;
}
.slide-next-enter,.slide-next-leave, .slide-prev-leave-to
{
transform: translate(100%, 0);
}
.slide-next-leave-to, .slide-prev-enter, .slide-prev-leave {
transform: translate(-100%, 0);
}
- transition : swipe 속도 조절
- transform : 탭나타나고 사라지는 애니메이션 효과
최종
<template>
<div id="app">
<div class="tabs" ref="tabbar">
<div
class="tabitem"
:class="index === activetab ? 'active' : ''"
v-for="(tab, index) in items"
@click="switchtab(index)"
:key="index"
ref="tab"
>
{{ tab }}
</div>
<div
class="slider"
:style="'transform:translateX(' + activetab * tabwidth + 'px)'"
></div>
</div>
<div
ref="tcon"
class="tabcontainer"
v-on="
pointer
? {
pointerdown: ($event) => start($event),
pointermove: ($event) => move($event),
pointerup: ($event) => end($event),
}
: {
touchstart: ($event) => start($event),
touchmove: ($event) => move($event),
touchend: ($event) => end($event),
}
"
>
<transition :name="transition" v-for="(tab, index) in items" :key="index">
<div class="tabpane" v-if="index === activetab">
{{ tab }} : Tab Content
</div>
</transition>
</div>
<h2>Source Code :</h2>
<a
href="https://gist.github.com/dagalti/eea7f49378d9fa21de5e359dde0f859a"
target="blank"
>Vue Simple Swipable Material Tabs</a
>
</div>
</template>
<script>
export default {
data() {
return {
transition: "slide-next",
activetab: 0,
tabwidth: 135,
items: [
"Google",
"Apple",
"Microsoft",
"Samsung",
"Nokia",
"Sony",
"Vivo",
"Oneplus",
"Oppo",
"LG",
"Blackberry",
],
touch: { sx: null, sy: null, st: null, ex: null, ey: null, et: null },
};
},
mounted() {
this.$refs.tabbar.style.setProperty("--tabwidth", this.tabwidth + "px");
},
computed: {
pointer() {
if (window.PointerEvent) return true;
else return false;
},
},
methods: {
switchtab(n) {
let scroll, scond;
if (this.activetab > n) {
this.transition = "slide-prev";
scroll = n - 1;
if (scond)
this.$refs.tab[scroll].scrollIntoView({ behavior: "smooth" });
} else if (this.activetab < n) {
this.transition = "slide-next";
scroll = n + 1;
}
scond = scroll >= 0 && scroll < this.items.length;
if (scond) this.$refs.tab[scroll].scrollIntoView({ behavior: "smooth" });
this.$nextTick((_) => {
this.activetab = n;
});
},
start(e) {
this.settouchpos(e, "start");
},
move(e) {
this.settouchpos(e, "move");
},
end(e) {
this.settouchpos(e, "end");
let dx = this.touch.sx - this.touch.ex,
dy = this.touch.sy - this.touch.ey,
dt = this.touch.et - this.touch.st,
dir = Math.sign(dx),
ntab = this.activetab + dir,
vmove = Math.abs(dx) / Math.abs(dy) < Math.sqrt(4);
ntab = ntab >= 0 && ntab < this.items.length ? ntab : null;
if (Math.abs(dx) > 10 && ntab !== null && !vmove && dt < 300)
this.switchtab(ntab);
},
settouchpos(e, event) {
let ev = e.changedTouches ? e.changedTouches[0] : e;
if (event === "start") {
this.touch.sx = ev.clientX;
this.touch.sy = ev.clientY;
this.touch.st = Date.now();
} else {
this.touch.ex = ev.clientX;
this.touch.ey = ev.clientY;
this.touch.et = Date.now();
}
},
},
};
</script>
<style >
body {
font-family: "Roboto", Helvetica, sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
margin: 0;
}
.slide-next-leave-active,
.slide-next-enter-active,
.slide-prev-enter-active,
.slide-prev-leave-active {
transition: 0.5s;
}
.slide-next-enter,
.slide-next-leave,
.slide-prev-leave-to {
transform: translate(100%, 0);
}
.slide-next-leave-to,
.slide-prev-enter,
.slide-prev-leave {
transform: translate(-100%, 0);
}
.tabs {
display: flex;
position: relative;
background: #1565c0;
color: #f1f1f1;
height: 48px;
box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.2), 0 3px 4px 0 rgba(0, 0, 0, 0.14),
0 1px 7px 0 rgba(0, 0, 0, 0.12);
overflow-x: scroll;
overflow: -moz-scrollbars-none;
-ms-overflow-style: none;
}
.tabs::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
}
.tabitem {
display: flex;
align-items: center;
justify-content: center;
min-width: var(--tabwidth);
cursor: pointer;
text-transform: uppercase;
font-size: 14px;
}
.tabitem.active {
font-weight: 500;
color: white;
}
.slider {
position: absolute;
bottom: 0px;
height: 2px;
width: var(--tabwidth);
background: white;
transition: 0.5s ease;
}
.tabcontainer {
height: 500px;
position: relative;
min-height: 100%;
width: 100%;
touch-action: pan-y;
}
.tabpane {
position: absolute;
width: 100%;
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
height: 300px;
}
#app {
margin: 0 auto;
text-align: center;
}
</style>
출처:
방법2.
index.vue
<template>
<div class="example">
<div class="tabs">
<TabItem
v-for="item in list"
v-bind="item" :key="item.id"
v-model="currentId"/>
</div>
<div class="contents">
<transition>
<section class="item" :key="currentId">
{{ current.content }}
</section>
</transition>
</div>
</div>
</template>
<script>
import TabItem from './TabItem.vue'
export default {
components: { TabItem },
data() {
return {
currentId: 1,
list: [
{ id: 1, label: 'Tab1', content: '콘텐츠1' },
{ id: 2, label: 'Tab2', content: '콘텐츠2' },
{ id: 3, label: 'Tab3', content: '콘텐츠3' }
]
}
},
computed: {
current() {
return this.list.find(el => el.id === this.currentId) || {}
}
}
}
</script>
<style scoped>
.contents {
position: relative;
overflow: hidden;
width: 280px;
border: 2px solid #000;
}
.item {
box-sizing: border-box;
padding: 10px;
width: 100%;
transition: all 0.8s ease;
}
/* 트랜지션 전용 스타일 */
.v-leave-active {
position: absolute;
}
.v-enter {
transform: translateX(-100%);
}
.v-leave-to {
transform: translateX(100%);
}
</style>
tab.vue
<template>
<button @click="$emit('input', id)" :class="[active, 'tab']">
{{ label }}
</button>
</template>
<script>
export default {
props: {
id: Number,
label: String,
value: Number
},
computed: {
active() {
return this.value === this.id ? 'active' : false
}
}
}
</script>
<style scoped>
.tab {
border-radius: 2px 2px 0 0;
background: #fff;
color: #311d0a;
line-height: 24px;
}
.tab:hover {
background: #eeeeee;
}
.active {
background: #f7c9c9;
}
</style>
출처
728x90
'Vue' 카테고리의 다른 글
Vue/Nuxt Element 높이 값 받아오기 (0) | 2021.05.21 |
---|---|
vue 페이지 이동시 input 창 자동 포커스 (2) | 2021.05.19 |
mousemove (0) | 2021.05.16 |
input에 1byte마다 자음마다 validation (0) | 2021.05.14 |
vue 개발 확장 플러그인 (0) | 2021.05.13 |
Comments