티스토리 뷰
when2Meet이라는 그룹 일정 조율 사이트를 리뉴얼하는 프로젝트에 참여하게 되었다. 해당 웹 사이트에서의 주요 기능 중 하나가 드래그 해서 되는 일정을 선택할 수 있도록 하는 건데, 그 부분이 핵심이라 팀장이 일주일 동안 공부하고, 간단하게 구현까지 해보라는 과제를 내주셨다! 그래서 이것저것 알아보니, 우리 프로젝트에 딱 알맞는 react-table-drag-select 라는 모듈이 있었으나 현재는 deprecated 되어 사용할 수 없었다. dragselect 라는 개인 개발자가 만든 모듈이나, 생명주기를 이용하여 구현을 하신 분도 있었다. 하지만, 리액트에서 생명주기 함수까지 사용하고 싶지는 않았고, 더 대중적인 모듈을 찾고자 했다. 그 와중에 발견한 한줄기 빛이 바로 react-selecto...!
미리 보는 공부 결과 화면
🎯 사용법
개발자 공식 github에 있는 내용을 가져왔다.
바로 이해가 되는 부분은 번역만 해두었고, 잘 이해가 안가는 부분은 두번째 주석으로 남겨두었다.
// 개발자 공식 github에서 사용방법 번역 및 이해를 위한 추가 설명
import Selecto from "selecto";
const selecto = new Selecto({
// selection element를 추가할 container
container: document.body,
// Selecto's root container (No transformed container. (default: null)
rootContainer: null,
// selection element를 드래그할 영역(default: container)
dragContainer: Element,
// 선택할 타깃. queryselector나 Element로 등록할 수 있다.
selectableTargets: [".target", document.querySelector(".target2")],
// 클릭에 따라 고를지 말지, (default: true)
// 드래그가 아닌 클릭에도 선택될 수 있도록 할 것인가?
selectByClick: true,
// 타깃 안에서부터 고를 수 있는지 없는지, (default: true)
// 타깃 안에서부터 드래그 할 수 있게 할 것인가?
selectFromInside: true,
// 선택 이후 선택된 타깃과 함께 다음 타깃을 고를지 말지, (타깃이 재선택 된다면 선택 취소)
// 두번째 드래그 시작시 이전의 모든 선택이 초기화시키지 않고 유지할 것인가?
continueSelect: false,
// keydown과 keyup을 통해 다음 타깃 선택을 계속할 키 결정
// 특정 키를 누르면서 드래그하면 선택이 초기화됨, 어떤 키로?
toggleContinueSelect: "shift",
// keydown keyup 이벤트를 위한 container
keyContainer: window,
// 얼마큼 객체가 드래그 영역에 포함되어야 선택된걸로 할건지(default: 100)
hitRate: 100,
});
selecto.on("select", e => {
e.added.forEach(el => {
el.classList.add("selected");
});
e.removed.forEach(el => {
el.classList.remove("selected");
});
});
이번 프로젝트에서 쓰일 속성 값들을 미리 뽑아내기도 하고,
dragContainer: {element}, // 이 속성을 타깃 자체만으로 해야 타깃 위에서만 드래그 가능.
selectByClick: {true},
selectFromInside: {true},
continueSelect: {true}
hitRate:{0},
💻 적용
한줄짜리 timeline과 일주일짜리 timeline을 만들어보았다...!
1) 한줄 짜리 timeline
// App.js
import * as React from "react";
import Selecto from "react-selecto";
export default function App() {
const slots = [];
for (let i = 0; i < 96; ++i) {
slots.push(i);
}
return <div className="app">
<div className="container">
<Selecto
dragContainer={".slot"}
selectableTargets={[".selecto-area .slot"]}
hitRate={0}
selectByClick={true}
selectFromInside={true}
continueSelect={true}
ratio={0}
onSelect={e => {
e.added.forEach(el => {
el.classList.add("selected");
});
e.removed.forEach(el => {
el.classList.remove("selected");
});
}}
/>
<div className="select-container">
<div className="elements selecto-area" id="selecto1">
{slots.map(i => {
let sep = "";
if (i % 4 === 3) {
sep = " solid";
} else if (i % 4===1) {
sep = " dotted";
}
return <div className={"slot" + sep} key={i}></div>
}
)}
</div>
</div>
<div className="empty elements"></div>
</div>
</div>;
}
/* css */
html, body, #root {
position: relative;
margin: 0;
padding: 0;
height: 100%;
color: #333;
background: #fdfdfd;
}
.app {
position: relative;
min-height: 100%;
padding: 10px 20px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.container {
max-width: 800px;
}
body {
background: #fff;
}
.slot {
display: block;
width: 45px;
height: 10px;
background: #eee;
--color: #4af;
border-right: 0.5px solid black;
border-left: 0.5px solid black;
transition: all ease 0.2s;
}
.solid {
border-bottom: 0.5px solid black;
}
.dotted {
border-bottom: 0.5px dotted black;
}
.elements {
margin-top: 40px;
border: 0.5px solid black;
}
.selecto-area .selected {
color: #fff;
background: var(--color);
}
.scroll {
overflow: auto;
padding-top: 10px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.infinite-viewer, .scroll {
width: 100%;
height: 300px;
box-sizing: border-box;
}
.infinite-viewer .viewport {
padding-top: 10px;
}
.empty.elements {
border: none;
}
2) 일주일 timeline
// App.js
import * as React from "react";
import Selecto from "react-selecto";
export default function App() {
const slots = [];
for (let i = 0; i < 672; ++i) {
slots.push(i);
}
return <div className="app">
<div className="container">
<Selecto
dragContainer={".slot"}
selectableTargets={[".selecto-area .slot"]}
hitRate={0}
selectByClick={true}
selectFromInside={true}
continueSelect={true}
ratio={0}
onSelect={e => {
e.added.forEach(el => {
el.classList.add("selected");
});
e.removed.forEach(el => {
el.classList.remove("selected");
});
}}
/>
<div className="select-container">
<div className="elements selecto-area" id="selecto1">
{slots.map(i => {
let sep = ""
if (parseInt(i / 7) % 4 === 1) {
sep = "dotted";
} else if (parseInt(i / 7) % 4 === 3) {
sep = "solid";
}
return <div className={"slot "+sep} key={i}></div>
})}
</div>
</div>
<div className="empty elements"></div>
</div>
</div>;
}
/* CSS */
html, body, #root {
position: relative;
margin: 0;
padding: 0;
height: 100%;
color: #333;
background: #fdfdfd;
}
.app {
position: relative;
min-height: 100%;
padding: 10px 20px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
line-height: 0px;
}
body {
background: #fff;
}
.slot {
display: inline-block;
width: 45px;
height: 10px;
background: #eee;
--color: #4af;
box-sizing: border-box;
border-right: 0.5px solid black;
border-left: 0.5px solid black;
transition: all ease 0.2s;
}
.solid {
border-bottom: 0.5px solid black;
}
.dotted {
border-bottom: 0.5px dotted black;
}
.elements {
width: 315px;
margin-top: 40px;
border: 0.5px solid black;
}
.selecto-area .selected {
color: #fff;
background: var(--color);
}
.scroll {
overflow: auto;
padding-top: 10px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.infinite-viewer, .scroll {
width: 100%;
height: 300px;
box-sizing: border-box;
}
.infinite-viewer .viewport {
padding-top: 10px;
}
.empty.elements {
border: none;
}
🎈 수정한 속성
예제 코드를 기반으로 수정한 속성들은 다음과 같다.
- dragContainer => slot 바깥에서는 드래그가 먹히지 않도록 slot 자체를 드래그 영역으로 잡았다.
- hitrate => 영역이 조금만 드래그 영역에 들어가도 선택되도록 0으로 설정하였다.(낮을수록 민감해짐)
- 그 외 CSS 속성들.. => 가로에 7개의 slot만이 들어갈 수 있도록, slot과 전체 div width속성을 고정.
현재 일주일짜리 timeline은 CSS를 위해 가로 너비가 7일이라는 가정 하에 관계식을 세웠지만, 실제 프로젝트에 적용하기 위해서라도 더 일반적으로 나타낼 수 있는 방법을 생각해봐야할 것 같다!
'Front-end > React' 카테고리의 다른 글
[React] && 를 이용한 조건부 렌더링, 불필요한 0 렌더링 이유 (0) | 2022.08.07 |
---|---|
[React] JSX와 Props란? JSX와 Props 특징 요약 (0) | 2022.07.24 |