See the Pen Slider with 2 items by Oh Ikmyeong (@dpffpself) on CodePen.
한 화면에 2개씩 보여야 하는 슬라이더.
움직일때도 2개씩 움직인다.
그에 따른 페이저 갯수 및 컨트롤러 방법...

HTML
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2개만 보여주는 슬라이더</title>
<link rel="stylesheet" href="./css/style.css">
<script src="./js/main.js" type="module"></script>
</head>
<body>
<label>
<span>아이템 갯수</span>
<input type="number" name="idxcount" min="2" max="50" value="3" />
</label>
<section id="wrapper">
<div id="viewer"></div>
<div id="pager"></div>
</section>
</body>
</html>
|
cs |
CSS
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
@charset "UTF-8";
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
li{
list-style-type:none;
}
body{
display:flex;flex-flow:column nowrap;
justify-content:center; align-items:center;
gap:20px;
min-height:100vh;
background:#eee;
}
#wrapper{
--_gap:20px;
display:flex; flex-flow:column nowrap;
justify-content:flex-start; align-items:center;
gap:40px;
position:relative;
overflow:hidden;
width:100%;max-width:960px;
padding-bottom:calc(var(--_gap) * 2);
background:#fff;
}
#viewer{
position:relative; overflow:hidden;
width:100%;
padding:var(--_gap) clamp(10px,2.4vw,20px) 0;
}
#slider{
display:flex;flex-flow:row nowrap;
justify-content:flex-start; align-items:flex-start;
gap:var(--_gap);
position:relative;
transition:transform .2s linear;
}
.slider-item{
flex:none;
display:flex;flex-flow:column nowrap;
justify-content:center; align-items:center;
position:relative;
width:calc((100% - var(--_gap)) / 2);
aspect-ratio:1/1.3;
border:1px solid black;
font-size:5vw;
user-select:none;
}
#pager{
display:flex;flex-flow:row wrap;
justify-content:center; align-items:center;
gap:10px;
position:relative;
width:100%;
}
.pager-btn{
width:40px;
aspect-ratio:1/1;
}
.pager-btn.on{
background:#000;
color:#fff;
}
.pager-btn.hide{
display:none;
}
|
cs |
main.js
|
1
2
3
4
5
|
import { SliderCtrl } from "./ctrl.js";
import { SliderView } from "./view.js";
new SliderView().init();
new SliderCtrl().init();
|
cs |
view.js
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
import { SliderModel } from "./model.js";
export class SliderView {
constructor() {
this.$ipt = document.getElementsByName("idxcount")?.[0];
this.$viewer = document.getElementById("viewer");
this.$pager = document.getElementById("pager");
}
init() {
this.reset();
this.$ipt.addEventListener("change", () => {
this.reset();
});
}
reset() {
this.$viewer.innerHTML = "";
this.$pager.innerHTML = "";
SliderModel.itemLength = Number(this.$ipt.value) || 2;
SliderModel.idxCurr = 0;
this.draw_slider();
this.draw_pager();
}
draw_slider() {
const $list = document.createElement("UL");
$list.id = "slider";
this.$viewer.appendChild($list);
for (let i = 0; i < SliderModel.itemLength; i++) {
const $item = document.createElement("LI");
$item.classList.add(`slider-item`);
$item.textContent = i;
$list.appendChild($item);
}
}
draw_pager() {
for (let idx = 0; idx < SliderModel.itemLength; idx++) {
const $btn = document.createElement("BUTTON");
$btn.type = "button";
$btn.classList.add("pager-btn");
$btn.dataset.idx = idx;
if (idx === 0) {
$btn.classList.add("on");
}
if (idx === SliderModel.itemLength - 1) {
$btn.classList.add("hide");
}
if (SliderModel.itemLength > 3) {
if (SliderModel.itemLength % 2) {
if (idx % 2 && idx !== SliderModel.itemLength - 2) {
$btn.classList.add("hide");
}
} else {
if (idx % 2) {
$btn.classList.add("hide");
}
}
}
$btn.textContent = idx;
this.$pager.appendChild($btn);
}
}
}//class:SliderView
|
cs |
ctrl.js
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
import { SliderModel } from "./model.js";
export class SliderCtrl {
constructor() {
this.$viewer = document.getElementById("viewer");
this.$pager = document.getElementById("pager");
this.pos = {
x: null,
y: null
}
}
static get_list() {
const $list = document.getElementById("slider");
if (!$list) { throw new Error("no DOM list"); }
return $list;
}//get_list
/**
*
*/
get_left() {
const itemWidth = document.querySelector(".slider-item").offsetWidth;
return `(${itemWidth * -1}px * ${SliderModel.idxCurr}) - (var(--_gap) * ${SliderModel.idxCurr})`;
}//get_left
/**
*
*/
init() {
SliderModel.idxCurr = 0;
/* click */
this.$pager.addEventListener("click", this.on_click_pager);
/* touch, mouse */
this.$viewer.addEventListener("touchstart", this.on_downtouchstart, { once: true });
this.$viewer.addEventListener("mousedown", this.on_downtouchstart, { once: true });
}//init
/**
*
* @param {*} e
*/
on_downtouchstart = (e) => {
this.pos.x = e?.touches?.[0]?.clientX || e.clientX;
this.pos.y = e?.touches?.[0]?.clientY || e.clientY;
window.addEventListener("mousemove", this.on_move);
window.addEventListener("touchmove", this.on_move, { passive: false });
const when_end = (ed) => {
this.touchendmouseup(ed);
/* 다시 */
this.$viewer.addEventListener("touchstart", this.on_downtouchstart, { once: true });
this.$viewer.addEventListener("mousedown", this.on_downtouchstart, { once: true });
/* 터치 무브 지우기 */
window.removeEventListener("touchmove", this.on_move);
window.removeEventListener("mousemove", this.on_move);
}
window.addEventListener("mouseup", when_end, { once: true });
window.addEventListener("touchend", when_end, { once: true });
}//on_downtouchstart
/**
*
* @param {*} e
*/
on_move = (e) => {
const x = e?.touches?.[0]?.clientX || e.clientX;
const y = e?.touches?.[0]?.clientY || e.clientY;
const moveX = this.pos.x - x;
const moveY = this.pos.y - y;
const $list = SliderCtrl.get_list();
const leftDeafult = this.get_left();
/* 상하 이동 */
if (Math.abs(moveX) < Math.abs(moveY) && Math.abs(moveY) > 50) {
console.log("스크롤")
return;
}
if (e.clientX === undefined) {
console.log("모바일 좌우 이동이라 스크롤 못하게 할거임");
e.preventDefault();
}
$list.style.transform = `translateX(calc(${leftDeafult} - ${moveX / 1.5}px))`;
}//on_move
/**
*
* @param {*} e
*/
touchendmouseup = (e) => {
const endX = e?.changedTouches?.[0]?.clientX || e.clientX;
const endY = e?.changedTouches?.[0]?.clientY || e.clientY;
const moveX = this.pos.x - endX;
const amountX = Math.abs(moveX);
if (e?.changedTouches?.[0]?.clientX) {
if (amountX < 50) {
this.move_slider_after_drag(0);
return;
}
const amountY = Math.abs(this.pos.y - endY);
if (amountY >= 50) {
this.move_slider_after_drag(0);
return;
}
if (amountY >= amountX) {
this.move_slider_after_drag(0);
return;
}
}
this.move_slider_after_drag(moveX);
}//touchendmouseup
/**
*
* @param {*} e
*/
on_click_pager = (e) => {
const $btn = e.target;
if (!$btn.classList.contains("pager-btn")) { return; }
const idx = parseInt($btn.dataset.idx);
SliderModel.idxCurr = idx;
/* 슬라이더 움직이기 */
this.move_slider();
/* 페이저 토글 */
this.toggle_pager();
}//on_click_pager
/**
* 슬라이더 움직이기
*/
move_slider() {
const $list = SliderCtrl.get_list();
const left = this.get_left();
$list.style.transform = `translateX(calc(${left}))`;
}
/**
*
* @param {*} moveX
*/
move_slider_after_drag(moveX) {
const MAX = SliderModel.itemLength - 2;
let amount = 2;
const idxLast = SliderModel.idxCurr;
if (moveX > 0) {
SliderModel.idxCurr = idxLast + amount;
if (SliderModel.idxCurr >= MAX) {
SliderModel.idxCurr = MAX;
}
} else if (moveX < 0) {
if (SliderModel.itemLength > 3 && SliderModel.idxCurr === MAX && SliderModel.itemLength % 2) {
SliderModel.idxCurr = idxLast - 1;
} else {
SliderModel.idxCurr = idxLast - amount;
}
if (SliderModel.idxCurr <= 0) { SliderModel.idxCurr = 0; }
}
this.move_slider();
this.toggle_pager();
}
/**
*
*/
toggle_pager() {
const $$btn = document.querySelectorAll(".pager-btn");
$$btn.forEach($btn => {
const idx = parseInt($btn.dataset.idx);
$btn.classList.toggle("on", idx === SliderModel.idxCurr);
});
}
}//class:SliderCtrl
|
cs |
model.js
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
export class SliderModel{
#IDXCURR;
#itemLength;
constructor(){
if(SliderModel.instance){
return SliderModel.instance;
}
SliderModel.instance = this;
}
/* 현재 인덱스 */
set idxCurr(idx){
this.#IDXCURR = idx;
}
get idxCurr(){
return this.#IDXCURR;
}
/* 아이템 갯수 */
set itemLength(itemLength){
this.#itemLength = itemLength;
}
get itemLength(){
return this.#itemLength;
}
}
|
cs |
'CSS&JS > ⚡Thinkers' 카테고리의 다른 글
| [JS] position:absolute로 masonry layout 구현하기 (0) | 2026.01.09 |
|---|---|
| [CSS,JS] 삼각함수로 그래프와 그래프 윗중간점 사이를 잇는 선 구하기 (1) | 2025.12.11 |
| [CSS] DOM 2개로 Marquee 구현하기 (0) | 2025.07.15 |
| [JS]무작위로 LOVE 나올 때 까지 문자열 출력 (0) | 2024.05.20 |
| [JS,Canvas] 캔버스로 화살표 선 만들기 (0) | 2023.11.29 |